commit 97885e8ab27e03d3c2706fc7d81a08b241027b10
parent 89ebe089db333a60416a1c7280df1bfb49639857
Author: Damian Myrda <damian@prime8.dev>
Date: Tue, 31 Mar 2026 18:10:56 -0500
feat: migrate portfolio to Go HTMX backend
Replace the Astro stack with Go html/template SSR routes and API handlers, then consolidate client behavior into modular JS bundled with esbuild for stable HTMX interactions and deep-link UX.
Diffstat:
141 files changed, 5683 insertions(+), 7153 deletions(-)
diff --git a/.air.toml b/.air.toml
@@ -0,0 +1,18 @@
+root = "."
+tmp_dir = "build"
+
+[build]
+ cmd = "mkdir -p ./build && go run ./cmd/buildjs && go build -o ./build/server ./cmd/server"
+ bin = "./build/server"
+ include_ext = ["go", "gohtml", "html", "css", "js", "yaml", "yml", "md"]
+ exclude_dir = ["build", "vendor", "node_modules"]
+ exclude_file = ["static/app.js"]
+ delay = 200
+ stop_on_error = true
+ send_interrupt = true
+
+[log]
+ time = true
+
+[screen]
+ clear_on_rebuild = true
diff --git a/.gitignore b/.gitignore
@@ -1,30 +1,11 @@
-# dependencies
-node_modules/
-
-# build output
-dist/
-
-# Astro
-.astro/
-
# environment variables
.env
.env.local
.env.*.local
-# macOS
-.DS_Store
-Thumbs.db
-
-# IDE
-.vscode/
-.idea/
-*.swp
-*.swo
-*~
+# build output
+build/
+static/app.js
-# logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
+# opencode
+.opencode/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
@@ -1,8 +1,14 @@
repos:
- repo: local
hooks:
- - id: lint-staged
- name: lint-staged
- entry: sh -c 'cd website && bunx lint-staged'
+ - id: gofmt
+ name: gofmt
+ entry: gofmt -w
+ language: system
+ types: [go]
+
+ - id: go-test
+ name: go-test
+ entry: go test ./...
language: system
pass_filenames: false
diff --git a/.prettierignore b/.prettierignore
@@ -1,3 +0,0 @@
-dist
-node_modules
-bun.lock
diff --git a/.prettierrc b/.prettierrc
@@ -1,16 +0,0 @@
-{
- "semi": true,
- "singleQuote": false,
- "tabWidth": 2,
- "trailingComma": "all",
- "printWidth": 80,
- "plugins": ["prettier-plugin-astro"],
- "overrides": [
- {
- "files": "*.astro",
- "options": {
- "parser": "astro"
- }
- }
- ]
-}
diff --git a/Dockerfile b/Dockerfile
@@ -1,22 +1,23 @@
-FROM oven/bun:1 AS base
+FROM golang:latest AS build
WORKDIR /app
-FROM base AS deps
-COPY package.json bun.lock ./
-RUN bun install --frozen-lockfile
+COPY go.mod go.sum ./
+RUN go mod download
-FROM base AS build
-COPY --from=deps /app/node_modules ./node_modules
COPY . .
-RUN bun run build
-FROM oven/bun:1-slim AS runtime
+RUN go run ./cmd/buildjs
+
+RUN CGO_ENABLED=0 GOOS=linux go build -o /server ./cmd/server
+
+FROM alpine:latest
WORKDIR /app
-COPY --from=build /app/dist ./dist
-COPY --from=build /app/node_modules ./node_modules
-ENV HOST=0.0.0.0
-ENV PORT=4321
-EXPOSE 4321
+COPY --from=build /server ./server
+COPY --from=build /app/static ./static
+COPY --from=build /app/content ./content
+
+ENV PORT=3000
+EXPOSE 3000
-CMD ["bun", "./dist/server/entry.mjs"]
+CMD ["./server"]
diff --git a/README.md b/README.md
@@ -1,35 +1,35 @@
-# web
+# prime8.dev backend
-Astro web application for [prime8.dev](https://prime8.dev).
+Go + html/template backend for [prime8.dev](https://prime8.dev).
## Stack
-- **Astro** - Static site generator with SSR
-- **TypeScript** - Type-safe code
-- **Bun** - Package manager and runtime
-- **Docker** - Containerization
+- Go
+- Chi
+- HTMX
## Development
-Install dependencies:
+Run locally:
```sh
-bun install
+go run ./cmd/buildjs
+go run ./cmd/server
```
-Run development server:
+Run with hot reload (air):
```sh
-bun run dev
+air
```
-Build for production:
+Run tests:
```sh
-bun run build
+go test ./...
```
-Build the Docker image:
+Build Docker image:
```sh
docker build -t prime8dev .
diff --git a/astro.config.ts b/astro.config.ts
@@ -1,18 +0,0 @@
-import { defineConfig } from "astro/config";
-import bun from "astro-bun-adapter";
-import { SITE_URL } from "./src/constants/website";
-
-export default defineConfig({
- output: "server",
- site: SITE_URL,
- trailingSlash: "never",
- adapter: bun(),
- server: {
- host: true,
- },
- image: {
- service: {
- entrypoint: "astro/assets/services/noop",
- },
- },
-});
diff --git a/bun.lock b/bun.lock
@@ -1,1264 +0,0 @@
-{
- "lockfileVersion": 1,
- "configVersion": 1,
- "workspaces": {
- "": {
- "name": "prime8dev",
- "dependencies": {
- "astro": "^5.17.2",
- "astro-bun-adapter": "^1.0.2",
- "gray-matter": "^4.0.3",
- "jimp": "^1.6.0",
- "marked": "^17.0.2",
- },
- "devDependencies": {
- "@eslint/js": "^9.39.2",
- "eslint": "^9.39.2",
- "eslint-config-prettier": "^10.1.8",
- "eslint-plugin-astro": "^1.6.0",
- "globals": "^17.3.0",
- "lint-staged": "^16.2.7",
- "prettier": "^3.8.1",
- "prettier-plugin-astro": "^0.14.1",
- "typescript-eslint": "^8.56.0",
- },
- },
- },
- "packages": {
- "@astrojs/compiler": ["@astrojs/compiler@2.13.0", "", {}, "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw=="],
-
- "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.5", "", {}, "sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA=="],
-
- "@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.10", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.5", "@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.19.0", "smol-toml": "^1.5.2", "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-kk4HeYR6AcnzC4QV8iSlOfh+N8TZ3MEStxPyenyCtemqn8IpEATBFMTJcfrNW32dgpt6MY3oCkMM/Tv3/I4G3A=="],
-
- "@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
-
- "@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=="],
-
- "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
-
- "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
-
- "@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="],
-
- "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="],
-
- "@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="],
-
- "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
-
- "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
-
- "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
-
- "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
-
- "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
-
- "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
-
- "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
-
- "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
-
- "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
-
- "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
-
- "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
-
- "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
-
- "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
-
- "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
-
- "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
-
- "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
-
- "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
-
- "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
-
- "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
-
- "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
-
- "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
-
- "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
-
- "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
-
- "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
-
- "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
-
- "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
-
- "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
-
- "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
-
- "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
-
- "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
-
- "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
-
- "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
-
- "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="],
-
- "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="],
-
- "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
-
- "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
-
- "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
-
- "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
-
- "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
-
- "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
-
- "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
-
- "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
-
- "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
-
- "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
-
- "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
-
- "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
-
- "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
-
- "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
-
- "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
-
- "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
-
- "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
-
- "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
-
- "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
-
- "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
-
- "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
-
- "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
-
- "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
-
- "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
-
- "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
-
- "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
-
- "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
-
- "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
-
- "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
-
- "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
-
- "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
-
- "@jimp/core": ["@jimp/core@1.6.0", "", { "dependencies": { "@jimp/file-ops": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^16.0.0", "mime": "3" } }, "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w=="],
-
- "@jimp/diff": ["@jimp/diff@1.6.0", "", { "dependencies": { "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "pixelmatch": "^5.3.0" } }, "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw=="],
-
- "@jimp/file-ops": ["@jimp/file-ops@1.6.0", "", {}, "sha512-Dx/bVDmgnRe1AlniRpCKrGRm5YvGmUwbDzt+MAkgmLGf+jvBT75hmMEZ003n9HQI/aPnm/YKnXjg/hOpzNCpHQ=="],
-
- "@jimp/js-bmp": ["@jimp/js-bmp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "bmp-ts": "^1.0.9" } }, "sha512-FU6Q5PC/e3yzLyBDXupR3SnL3htU7S3KEs4e6rjDP6gNEOXRFsWs6YD3hXuXd50jd8ummy+q2WSwuGkr8wi+Gw=="],
-
- "@jimp/js-gif": ["@jimp/js-gif@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "gifwrap": "^0.10.1", "omggif": "^1.0.10" } }, "sha512-N9CZPHOrJTsAUoWkWZstLPpwT5AwJ0wge+47+ix3++SdSL/H2QzyMqxbcDYNFe4MoI5MIhATfb0/dl/wmX221g=="],
-
- "@jimp/js-jpeg": ["@jimp/js-jpeg@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "jpeg-js": "^0.4.4" } }, "sha512-6vgFDqeusblf5Pok6B2DUiMXplH8RhIKAryj1yn+007SIAQ0khM1Uptxmpku/0MfbClx2r7pnJv9gWpAEJdMVA=="],
-
- "@jimp/js-png": ["@jimp/js-png@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "pngjs": "^7.0.0" } }, "sha512-AbQHScy3hDDgMRNfG0tPjL88AV6qKAILGReIa3ATpW5QFjBKpisvUaOqhzJ7Reic1oawx3Riyv152gaPfqsBVg=="],
-
- "@jimp/js-tiff": ["@jimp/js-tiff@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "utif2": "^4.1.0" } }, "sha512-zhReR8/7KO+adijj3h0ZQUOiun3mXUv79zYEAKvE0O+rP7EhgtKvWJOZfRzdZSNv0Pu1rKtgM72qgtwe2tFvyw=="],
-
- "@jimp/plugin-blit": ["@jimp/plugin-blit@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M+uRWl1csi7qilnSK8uxK4RJMSuVeBiO1AY0+7APnfUbQNZm6hCe0CCFv1Iyw1D/Dhb8ph8fQgm5mwM0eSxgVA=="],
-
- "@jimp/plugin-blur": ["@jimp/plugin-blur@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-zrM7iic1OTwUCb0g/rN5y+UnmdEsT3IfuCXCJJNs8SZzP0MkZ1eTvuwK9ZidCuMo4+J3xkzCidRwYXB5CyGZTw=="],
-
- "@jimp/plugin-circle": ["@jimp/plugin-circle@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-xt1Gp+LtdMKAXfDp3HNaG30SPZW6AQ7dtAtTnoRKorRi+5yCJjKqXRgkewS5bvj8DEh87Ko1ydJfzqS3P2tdWw=="],
-
- "@jimp/plugin-color": ["@jimp/plugin-color@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "tinycolor2": "^1.6.0", "zod": "^3.23.8" } }, "sha512-J5q8IVCpkBsxIXM+45XOXTrsyfblyMZg3a9eAo0P7VPH4+CrvyNQwaYatbAIamSIN1YzxmO3DkIZXzRjFSz1SA=="],
-
- "@jimp/plugin-contain": ["@jimp/plugin-contain@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-oN/n+Vdq/Qg9bB4yOBOxtY9IPAtEfES8J1n9Ddx+XhGBYT1/QTU/JYkGaAkIGoPnyYvmLEDqMz2SGihqlpqfzQ=="],
-
- "@jimp/plugin-cover": ["@jimp/plugin-cover@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Iow0h6yqSC269YUJ8HC3Q/MpCi2V55sMlbkkTTx4zPvd8mWZlC0ykrNDeAy9IJegrQ7v5E99rJwmQu25lygKLA=="],
-
- "@jimp/plugin-crop": ["@jimp/plugin-crop@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-KqZkEhvs+21USdySCUDI+GFa393eDIzbi1smBqkUPTE+pRwSWMAf01D5OC3ZWB+xZsNla93BDS9iCkLHA8wang=="],
-
- "@jimp/plugin-displace": ["@jimp/plugin-displace@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-4Y10X9qwr5F+Bo5ME356XSACEF55485j5nGdiyJ9hYzjQP9nGgxNJaZ4SAOqpd+k5sFaIeD7SQ0Occ26uIng5Q=="],
-
- "@jimp/plugin-dither": ["@jimp/plugin-dither@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0" } }, "sha512-600d1RxY0pKwgyU0tgMahLNKsqEcxGdbgXadCiVCoGd6V6glyCvkNrnnwC0n5aJ56Htkj88PToSdF88tNVZEEQ=="],
-
- "@jimp/plugin-fisheye": ["@jimp/plugin-fisheye@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-E5QHKWSCBFtpgZarlmN3Q6+rTQxjirFqo44ohoTjzYVrDI6B6beXNnPIThJgPr0Y9GwfzgyarKvQuQuqCnnfbA=="],
-
- "@jimp/plugin-flip": ["@jimp/plugin-flip@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-/+rJVDuBIVOgwoyVkBjUFHtP+wmW0r+r5OQ2GpatQofToPVbJw1DdYWXlwviSx7hvixTWLKVgRWQ5Dw862emDg=="],
-
- "@jimp/plugin-hash": ["@jimp/plugin-hash@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "any-base": "^1.1.0" } }, "sha512-wWzl0kTpDJgYVbZdajTf+4NBSKvmI3bRI8q6EH9CVeIHps9VWVsUvEyb7rpbcwVLWYuzDtP2R0lTT6WeBNQH9Q=="],
-
- "@jimp/plugin-mask": ["@jimp/plugin-mask@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Cwy7ExSJMZszvkad8NV8o/Z92X2kFUFM8mcDAhNVxU0Q6tA0op2UKRJY51eoK8r6eds/qak3FQkXakvNabdLnA=="],
-
- "@jimp/plugin-print": ["@jimp/plugin-print@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/types": "1.6.0", "parse-bmfont-ascii": "^1.0.6", "parse-bmfont-binary": "^1.0.6", "parse-bmfont-xml": "^1.1.6", "simple-xml-to-json": "^1.2.2", "zod": "^3.23.8" } }, "sha512-zarTIJi8fjoGMSI/M3Xh5yY9T65p03XJmPsuNet19K/Q7mwRU6EV2pfj+28++2PV2NJ+htDF5uecAlnGyxFN2A=="],
-
- "@jimp/plugin-quantize": ["@jimp/plugin-quantize@1.6.0", "", { "dependencies": { "image-q": "^4.0.0", "zod": "^3.23.8" } }, "sha512-EmzZ/s9StYQwbpG6rUGBCisc3f64JIhSH+ncTJd+iFGtGo0YvSeMdAd+zqgiHpfZoOL54dNavZNjF4otK+mvlg=="],
-
- "@jimp/plugin-resize": ["@jimp/plugin-resize@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA=="],
-
- "@jimp/plugin-rotate": ["@jimp/plugin-rotate@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-JagdjBLnUZGSG4xjCLkIpQOZZ3Mjbg8aGCCi4G69qR+OjNpOeGI7N2EQlfK/WE8BEHOW5vdjSyglNqcYbQBWRw=="],
-
- "@jimp/plugin-threshold": ["@jimp/plugin-threshold@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M59m5dzLoHOVWdM41O8z9SyySzcDn43xHseOH0HavjsfQsT56GGCC4QzU1banJidbUrePhzoEdS42uFE8Fei8w=="],
-
- "@jimp/types": ["@jimp/types@1.6.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-7UfRsiKo5GZTAATxm2qQ7jqmUXP0DxTArztllTcYdyw6Xi5oT4RaoXynVtCD4UyLK5gJgkZJcwonoijrhYFKfg=="],
-
- "@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="],
-
- "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
-
- "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
-
- "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
-
- "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
-
- "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
-
- "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
-
- "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
-
- "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.1", "", { "os": "android", "cpu": "arm" }, "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg=="],
-
- "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg=="],
-
- "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg=="],
-
- "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ=="],
-
- "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg=="],
-
- "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw=="],
-
- "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ=="],
-
- "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg=="],
-
- "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ=="],
-
- "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA=="],
-
- "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g=="],
-
- "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw=="],
-
- "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw=="],
-
- "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw=="],
-
- "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw=="],
-
- "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg=="],
-
- "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg=="],
-
- "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="],
-
- "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w=="],
-
- "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg=="],
-
- "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.1", "", { "os": "none", "cpu": "arm64" }, "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw=="],
-
- "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g=="],
-
- "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA=="],
-
- "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg=="],
-
- "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw=="],
-
- "@shikijs/core": ["@shikijs/core@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA=="],
-
- "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ=="],
-
- "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ=="],
-
- "@shikijs/langs": ["@shikijs/langs@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0" } }, "sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA=="],
-
- "@shikijs/themes": ["@shikijs/themes@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0" } }, "sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw=="],
-
- "@shikijs/types": ["@shikijs/types@3.21.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA=="],
-
- "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
-
- "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
-
- "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
-
- "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
-
- "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
-
- "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
-
- "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
-
- "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
-
- "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="],
-
- "@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="],
-
- "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
-
- "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/type-utils": "8.56.0", "@typescript-eslint/utils": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw=="],
-
- "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg=="],
-
- "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.0", "@typescript-eslint/types": "^8.56.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg=="],
-
- "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1" } }, "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ=="],
-
- "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg=="],
-
- "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0", "@typescript-eslint/utils": "8.56.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA=="],
-
- "@typescript-eslint/types": ["@typescript-eslint/types@8.53.1", "", {}, "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A=="],
-
- "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.0", "@typescript-eslint/tsconfig-utils": "8.56.0", "@typescript-eslint/types": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q=="],
-
- "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ=="],
-
- "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg=="],
-
- "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
-
- "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
-
- "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
-
- "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
-
- "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
-
- "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
-
- "ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="],
-
- "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
- "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
-
- "any-base": ["any-base@1.1.0", "", {}, "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="],
-
- "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
-
- "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
-
- "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
-
- "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="],
-
- "astro": ["astro@5.17.2", "", { "dependencies": { "@astrojs/compiler": "^2.13.0", "@astrojs/internal-helpers": "0.7.5", "@astrojs/markdown-remark": "6.3.10", "@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.0", "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-7jnMqGo53hOQNwo1N/wqeOvUp8wwW/p+DeerSjSkHNx8L/1mhy6P7rVo7EhdmF8DpKqw0tl/B5Fx1WcIzg1ysA=="],
-
- "astro-bun-adapter": ["astro-bun-adapter@1.0.2", "", { "peerDependencies": { "astro": "^4.0.7" } }, "sha512-ugiguVhpLq/O0HTA/DIokAGX9u/aOr+JiwR0++xURN6aAY9zjXU0dfj9A3V4KGXgNXMEEFiZ5gobMhcURAM3Fw=="],
-
- "astro-eslint-parser": ["astro-eslint-parser@1.3.0", "", { "dependencies": { "@astrojs/compiler": "^2.0.0", "@typescript-eslint/scope-manager": "^7.0.0 || ^8.0.0", "@typescript-eslint/types": "^7.0.0 || ^8.0.0", "astrojs-compiler-sync": "^1.0.0", "debug": "^4.3.4", "entities": "^6.0.0", "eslint-scope": "^8.0.1", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "fast-glob": "^3.3.3", "is-glob": "^4.0.3", "semver": "^7.3.8" } }, "sha512-aOLc/aDR7lTWAHlytEefwn4Y6qs6uMr69DZvUx2A1AOAZsWhGB/paiRWPtVchh9wzMvLeqr+DkbENhVreVr9AQ=="],
-
- "astrojs-compiler-sync": ["astrojs-compiler-sync@1.1.1", "", { "dependencies": { "synckit": "^0.11.0" }, "peerDependencies": { "@astrojs/compiler": ">=0.27.0" } }, "sha512-0mKvB9sDQRIZPsEJadw6OaFbGJ92cJPPR++ICca9XEyiUAZqgVuk25jNmzHPT0KF80rI94trSZrUR5iHFXGGOQ=="],
-
- "await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="],
-
- "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
-
- "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
-
- "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
-
- "base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="],
-
- "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
-
- "bmp-ts": ["bmp-ts@1.0.9", "", {}, "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw=="],
-
- "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=="],
-
- "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
-
- "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
-
- "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
-
- "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
-
- "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="],
-
- "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
-
- "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
-
- "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=="],
-
- "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
-
- "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
-
- "ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
-
- "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
-
- "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
-
- "cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="],
-
- "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
-
- "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
-
- "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
-
- "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
-
- "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
-
- "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
-
- "common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="],
-
- "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
-
- "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
-
- "cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
-
- "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
-
- "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
-
- "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
-
- "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
-
- "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=="],
-
- "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
-
- "decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="],
-
- "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
-
- "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
-
- "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
-
- "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
-
- "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.2", "", {}, "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg=="],
-
- "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
-
- "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
-
- "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
-
- "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
-
- "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
-
- "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
-
- "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
-
- "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
-
- "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
-
- "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
-
- "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
-
- "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
-
- "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
-
- "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
-
- "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
-
- "eslint-compat-utils": ["eslint-compat-utils@0.6.5", "", { "dependencies": { "semver": "^7.5.4" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-vAUHYzue4YAa2hNACjB8HvUQj5yehAZgiClyFVVom9cP8z5NSFq3PwB/TtJslN2zAMgRX6FCFCjYBbQh71g5RQ=="],
-
- "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
-
- "eslint-plugin-astro": ["eslint-plugin-astro@1.6.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@jridgewell/sourcemap-codec": "^1.4.14", "@typescript-eslint/types": "^7.7.1 || ^8", "astro-eslint-parser": "^1.3.0", "eslint-compat-utils": "^0.6.0", "globals": "^16.0.0", "postcss": "^8.4.14", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "eslint": ">=8.57.0" } }, "sha512-yGIbLHuj5MOUXa0s4sZ6cVhv6ehb+WLF80tsrGaxMk6VTUExruMzubQDzhOYt8fbR1c9vILCCRSCsKI7M1whig=="],
-
- "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
-
- "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
-
- "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
-
- "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
-
- "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
-
- "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
-
- "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
-
- "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
-
- "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
-
- "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
-
- "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="],
-
- "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
-
- "exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="],
-
- "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
-
- "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
-
- "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
-
- "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
-
- "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
-
- "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
-
- "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
-
- "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
-
- "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
-
- "file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
-
- "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
-
- "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
-
- "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
-
- "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
-
- "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
-
- "fontace": ["fontace@0.4.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-moThBCItUe2bjZip5PF/iZClpKHGLwMvR79Kp8XpGRBrvoRSnySN4VcILdv3/MJzbhvUA5WeiUXF5o538m5fvg=="],
-
- "fontkitten": ["fontkitten@1.0.1", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-m+/cO+/kAU9farlejecXLgQH20+UXyH0K6oosGtogAz7BWco+KTYE60epKwMt8eVxqlOE2Fs+GoHVlGDUbKOoA=="],
-
- "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
-
- "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
-
- "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
-
- "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
-
- "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
-
- "globals": ["globals@17.3.0", "", {}, "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw=="],
-
- "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
-
- "h3": ["h3@1.15.5", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg=="],
-
- "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
-
- "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
-
- "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
-
- "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="],
-
- "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
-
- "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
-
- "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
-
- "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
-
- "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="],
-
- "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
-
- "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
-
- "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
-
- "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
-
- "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
-
- "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
-
- "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
-
- "image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="],
-
- "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
-
- "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
-
- "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
-
- "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
-
- "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
-
- "is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
-
- "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
-
- "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
-
- "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
-
- "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
-
- "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
-
- "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=="],
-
- "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
-
- "jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="],
-
- "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
-
- "jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
-
- "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
-
- "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
-
- "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
-
- "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
-
- "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
-
- "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
-
- "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
-
- "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
-
- "lint-staged": ["lint-staged@16.2.7", "", { "dependencies": { "commander": "^14.0.2", "listr2": "^9.0.5", "micromatch": "^4.0.8", "nano-spawn": "^2.0.0", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.1" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow=="],
-
- "listr2": ["listr2@9.0.5", "", { "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g=="],
-
- "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
-
- "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
-
- "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="],
-
- "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
-
- "lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
-
- "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=="],
-
- "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
-
- "marked": ["marked@17.0.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-s5HZGFQea7Huv5zZcAGhJLT3qLpAfnY7v7GWkICUr0+Wd5TFEtdlRR2XUL5Gg+RH7u2Df595ifrxR03mBaw7gA=="],
-
- "mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="],
-
- "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
-
- "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
-
- "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
-
- "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
-
- "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
-
- "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
-
- "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
-
- "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
-
- "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
-
- "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
-
- "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
-
- "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
-
- "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
-
- "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
-
- "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
-
- "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
-
- "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
-
- "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
-
- "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
-
- "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
-
- "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
-
- "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
-
- "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
-
- "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
-
- "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
-
- "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
-
- "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
-
- "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
-
- "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
-
- "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
-
- "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
-
- "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
-
- "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
-
- "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
-
- "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
-
- "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
-
- "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
-
- "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
-
- "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
-
- "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
-
- "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
-
- "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
-
- "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
-
- "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
-
- "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
-
- "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
-
- "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
-
- "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
-
- "nano-spawn": ["nano-spawn@2.0.0", "", {}, "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw=="],
-
- "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
-
- "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
-
- "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
-
- "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
-
- "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
-
- "node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="],
-
- "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
-
- "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
-
- "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=="],
-
- "omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
-
- "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
-
- "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
-
- "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=="],
-
- "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
-
- "p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="],
-
- "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
-
- "p-queue": ["p-queue@8.1.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="],
-
- "p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="],
-
- "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
-
- "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
-
- "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
-
- "parse-bmfont-ascii": ["parse-bmfont-ascii@1.0.6", "", {}, "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA=="],
-
- "parse-bmfont-binary": ["parse-bmfont-binary@1.0.6", "", {}, "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA=="],
-
- "parse-bmfont-xml": ["parse-bmfont-xml@1.1.6", "", { "dependencies": { "xml-parse-from-string": "^1.0.0", "xml2js": "^0.5.0" } }, "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA=="],
-
- "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="],
-
- "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
-
- "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
-
- "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
-
- "peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="],
-
- "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="],
-
- "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
-
- "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
-
- "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="],
-
- "pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="],
-
- "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
-
- "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=="],
-
- "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
-
- "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
-
- "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
-
- "prettier-plugin-astro": ["prettier-plugin-astro@0.14.1", "", { "dependencies": { "@astrojs/compiler": "^2.9.1", "prettier": "^3.0.0", "sass-formatter": "^0.7.6" } }, "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw=="],
-
- "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
-
- "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
-
- "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=="],
-
- "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
-
- "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
-
- "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
-
- "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
-
- "readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="],
-
- "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
-
- "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
-
- "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
-
- "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
-
- "rehype": ["rehype@13.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="],
-
- "rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="],
-
- "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
-
- "rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="],
-
- "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
-
- "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
-
- "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
-
- "remark-smartypants": ["remark-smartypants@3.0.2", "", { "dependencies": { "retext": "^9.0.0", "retext-smartypants": "^6.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA=="],
-
- "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
-
- "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
-
- "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
-
- "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="],
-
- "retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="],
-
- "retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="],
-
- "retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="],
-
- "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
-
- "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
-
- "rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="],
-
- "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
-
- "s.color": ["s.color@0.0.15", "", {}, "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA=="],
-
- "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
-
- "sass-formatter": ["sass-formatter@0.7.9", "", { "dependencies": { "suf-log": "^2.5.3" } }, "sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw=="],
-
- "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="],
-
- "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
-
- "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
-
- "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=="],
-
- "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
-
- "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
-
- "shiki": ["shiki@3.21.0", "", { "dependencies": { "@shikijs/core": "3.21.0", "@shikijs/engine-javascript": "3.21.0", "@shikijs/engine-oniguruma": "3.21.0", "@shikijs/langs": "3.21.0", "@shikijs/themes": "3.21.0", "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w=="],
-
- "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
-
- "simple-xml-to-json": ["simple-xml-to-json@1.2.3", "", {}, "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA=="],
-
- "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
-
- "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
-
- "smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="],
-
- "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
-
- "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
-
- "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
-
- "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
-
- "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=="],
-
- "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
-
- "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
-
- "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
-
- "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
-
- "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
-
- "strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="],
-
- "suf-log": ["suf-log@2.5.3", "", { "dependencies": { "s.color": "0.0.15" } }, "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow=="],
-
- "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
-
- "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=="],
-
- "synckit": ["synckit@0.11.12", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="],
-
- "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
-
- "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
-
- "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
-
- "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
-
- "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
-
- "token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="],
-
- "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
-
- "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
-
- "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
-
- "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
-
- "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
-
- "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
-
- "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
-
- "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
-
- "typescript-eslint": ["typescript-eslint@8.56.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.0", "@typescript-eslint/parser": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0", "@typescript-eslint/utils": "8.56.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg=="],
-
- "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
-
- "ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="],
-
- "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
-
- "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
-
- "unifont": ["unifont@0.7.3", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-b0GtQzKCyuSHGsfj5vyN8st7muZ6VCI4XD4vFlr7Uy1rlWVYxC3npnfk8MyreHxJYrz1ooLDqDzFe9XqQTlAhA=="],
-
- "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
-
- "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
-
- "unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="],
-
- "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
-
- "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="],
-
- "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-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="],
-
- "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
-
- "unstorage": ["unstorage@1.17.4", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.5", "lru-cache": "^11.2.0", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw=="],
-
- "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
-
- "utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="],
-
- "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
-
- "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
-
- "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
-
- "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=="],
-
- "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=="],
-
- "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
-
- "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
-
- "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=="],
-
- "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
-
- "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=="],
-
- "xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="],
-
- "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
-
- "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
-
- "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
-
- "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
-
- "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
-
- "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=="],
-
- "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
-
- "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=="],
-
- "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
-
- "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
-
- "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
-
- "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0" } }, "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w=="],
-
- "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
-
- "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0" } }, "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w=="],
-
- "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.56.0", "", {}, "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ=="],
-
- "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.56.0", "", {}, "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ=="],
-
- "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg=="],
-
- "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.56.0", "", {}, "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ=="],
-
- "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.56.0", "", {}, "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ=="],
-
- "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
-
- "@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0" } }, "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w=="],
-
- "@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.56.0", "", {}, "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ=="],
-
- "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.56.0", "", {}, "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ=="],
-
- "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.0", "", {}, "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q=="],
-
- "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
-
- "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
-
- "boxen/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
-
- "cli-truncate/string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="],
-
- "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=="],
-
- "eslint-plugin-astro/globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
-
- "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
-
- "gray-matter/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
-
- "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
-
- "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
-
- "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
-
- "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
-
- "prettier-plugin-astro/prettier": ["prettier@3.8.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA=="],
-
- "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
-
- "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
-
- "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=="],
-
- "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
-
- "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.56.0", "", {}, "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ=="],
-
- "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
-
- "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
-
- "ansi-align/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
-
- "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
-
- "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
-
- "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
-
- "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
-
- "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=="],
-
- "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
- }
-}
diff --git a/cmd/buildjs/main.go b/cmd/buildjs/main.go
@@ -0,0 +1,29 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/evanw/esbuild/pkg/api"
+)
+
+func main() {
+ result := api.Build(api.BuildOptions{
+ EntryPoints: []string{"internal/view/scripts/app.js"},
+ Outfile: "static/app.js",
+ Bundle: true,
+ MinifyWhitespace: true,
+ MinifySyntax: true,
+ MinifyIdentifiers: false,
+ Target: api.ES2020,
+ Format: api.FormatIIFE,
+ Platform: api.PlatformBrowser,
+ Write: true,
+ LogLevel: api.LogLevelInfo,
+ })
+
+ if len(result.Errors) > 0 {
+ fmt.Fprintf(os.Stderr, "esbuild failed with %d errors\n", len(result.Errors))
+ os.Exit(1)
+ }
+}
diff --git a/cmd/server/main.go b/cmd/server/main.go
@@ -0,0 +1,64 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "time"
+
+ "git.prime8.dev/moncheeta/prime8dev/internal/content"
+ "git.prime8.dev/moncheeta/prime8dev/internal/handlers"
+ "github.com/go-chi/chi/v5"
+ "github.com/go-chi/chi/v5/middleware"
+ "github.com/joho/godotenv"
+)
+
+func main() {
+ _ = godotenv.Load()
+ if err := content.InitBlogs(); err != nil {
+ log.Fatalf("Failed to load blogs: %v", err)
+ }
+
+ r := chi.NewRouter()
+
+ r.Use(middleware.Logger)
+ r.Use(middleware.Recoverer)
+ r.Use(middleware.Timeout(30 * time.Second))
+
+ h := handlers.NewHandler()
+
+ fs := http.FileServer(http.Dir("./static"))
+ r.Handle("/static/*", http.StripPrefix("/static/", fs))
+ serveStaticFile := func(file string) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ http.ServeFile(w, r, "./static/"+file)
+ }
+ }
+ r.Get("/robots.txt", serveStaticFile("robots.txt"))
+ r.Get("/favicon.png", serveStaticFile("favicon.png"))
+ r.Get("/site.webmanifest", serveStaticFile("site.webmanifest"))
+
+ r.Get("/", h.HomeHandler)
+ r.Get("/projects", h.ProjectsListHandler)
+ r.Get("/projects/{id}", h.ProjectPageHandler)
+ r.Get("/experience", h.ExperienceListHandler)
+ r.Get("/experience/{id}", h.ExperiencePageHandler)
+ r.Get("/blog", h.BlogListHandler)
+ r.Get("/blog/{id}", h.BlogPageHandler)
+ r.Get("/sitemap.xml", h.SitemapHandler)
+
+ r.Get("/api/projects/{id}", h.ProjectDetailHandler)
+ r.Get("/api/blog/{id}", h.BlogDetailHandler)
+ r.Get("/api/wakatime", h.WakaTimeHandler)
+
+ port := os.Getenv("PORT")
+ if port == "" {
+ port = "3000"
+ }
+
+ log.Printf("Server starting on :%s", port)
+ if err := http.ListenAndServe(fmt.Sprintf(":%s", port), r); err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/content/blogs/building-my-portfolio.md b/content/blogs/building-my-portfolio.md
@@ -0,0 +1,42 @@
+---
+id: building-my-portfolio
+title: building a tui-inspired portfolio
+description: how i built this terminal-style portfolio with astro
+date: 2026-01-31
+tags:
+ - Astro
+ - TypeScript
+ - TUI
+ - Web Development
+ - Portfolio
+---
+
+Once I got used to the terminal, **I couldn't live without it**. I love the **simplicity** of the interface and the fact that it's **keyboard native** (no need to reach for the mouse). Anything can be reached within a few key presses, which is one of the characteristics that I love.
+
+## the stack
+
+I built this website using **Astro** using server-side rendering. I tried to keep it as simple as possible, no over-engineering. Don't need WASM, a full-fledged database, or a complex UI framework. Details about me are stored in json files for ease of access (no need to scour the entire project to make a change), and my blogs are stored in a directory of markdown files.
+
+## design decisions
+
+Some characteristics that I've observed in terminal user interfaces I've transferred over to my website:
+
+- **Keyboard-first navigation**: `Arrow keys` allows navigation throughout the UI. `Tab` and the number keys (`1-4`) can be used to switch between tabs instantly. `Enter` intuitively opens blog posts and `Escape` closes them.
+- **No stale content**: One thing I don't want to worry about it updating my website every time something changes. This is a design pattern that is followed in the entire project.
+ - Project details are fetched from my [self-hosted GitLab instance](https://git.prime8.dev).
+ - Blog metadata is taken from the file stats, not a hard coded value.
+ - Coding stats are pulled from [my Wakatime account](https://wakatime.com/@moncheeta).
+- **Personality**: One thing I want to make sure of is that this website is unique, but also derives some of its elements from existing terminal applications.
+ - Boot animation adds a feeling of life to the website, even though it's entirely static.
+ - Top tab bar is an idea taken from the countless GUI applications that have it.
+ - Bottom status bar is inspired by many terminal applications that use a similar setup.
+
+These characteristics make this website feel like the terminal applications that I'm in love with, while differentiating it to make it mine.
+
+## conclusion
+
+Building this portfolio was a chance for me to combine two things **I love**: terminal user interfaces and modern web development. The result is something that feels **personal**, a site that actually reflects how I actually work.
+
+If you're thinking about building your own portfolio, my advice is simple: **make it yours**. Pick a style that exemplifies you and add the features you'd actually want to use.
+
+The source code is available at [git.prime8.dev](https://git.prime8.dev/moncheeta/prime8dev)
diff --git a/env.example b/env.example
@@ -1,3 +1,3 @@
-NODE_ENV=dev|prod
-GITLAB_HOST=https://gitlab.com
+APP_ENV=dev
+PORT=3000
WAKATIME_API_KEY=your_wakatime_api_key_here
diff --git a/eslint.config.js b/eslint.config.js
@@ -1,24 +0,0 @@
-import js from "@eslint/js";
-import globals from "globals";
-import prettier from "eslint-config-prettier";
-import eslintPluginAstro from "eslint-plugin-astro";
-import tseslint from "typescript-eslint";
-
-export default [
- { ignores: ["dist", "node_modules", ".astro"] },
- js.configs.recommended,
- ...tseslint.configs.recommended,
- ...eslintPluginAstro.configs.recommended,
- prettier,
- {
- files: ["**/*.js", "**/*.ts"],
- languageOptions: {
- ecmaVersion: 2022,
- sourceType: "module",
- globals: {
- ...globals.browser,
- ...globals.node,
- },
- },
- },
-];
diff --git a/go.mod b/go.mod
@@ -0,0 +1,18 @@
+module git.prime8.dev/moncheeta/prime8dev
+
+go 1.23.0
+
+require (
+ github.com/go-chi/chi/v5 v5.2.5
+ github.com/patrickmn/go-cache v2.1.0+incompatible
+ github.com/yuin/goldmark v1.8.2
+)
+
+require (
+ github.com/evanw/esbuild v0.27.4
+ github.com/joho/godotenv v1.5.1
+ github.com/romance-dev/ascii-art v1.0.1
+ gopkg.in/yaml.v3 v3.0.1
+)
+
+require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
diff --git a/go.sum b/go.sum
@@ -0,0 +1,18 @@
+github.com/evanw/esbuild v0.27.4 h1:8opEixKkH9EDsdjxC/aPmpk1KPwQOcyknDo5m5xIFxI=
+github.com/evanw/esbuild v0.27.4/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
+github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
+github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
+github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
+github.com/romance-dev/ascii-art v1.0.1 h1:mBnFjBpZVNBkgKT/SRS/a3ToeabLq4yU3bFHgt4U/cI=
+github.com/romance-dev/ascii-art v1.0.1/go.mod h1:Bq49vKfrqe73mR/tyu2PSr4qPaS+tsX0cdk/7ZuZUqw=
+github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
+github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/config/about.go b/internal/config/about.go
@@ -0,0 +1,31 @@
+package config
+
+type About struct {
+ Name string
+ Handle string
+ Bio string
+ Location string
+ Profession string
+ ProfessionShort string
+ Languages []string
+ Interests []string
+ Tools []string
+ School string
+ Graduating string
+}
+
+func GetAbout() About {
+ return About{
+ Name: "damian myrda",
+ Handle: "moncheeta",
+ Bio: "passionate about low-level systems programming, tui/cli tooling, and unix philosophy. always learning just like curious george 🐒",
+ Location: "chicago, il",
+ Profession: "software engineer",
+ ProfessionShort: "swe",
+ Languages: []string{"zig", "rust", "c", "go", "typescript", "python"},
+ Interests: []string{"low-level systems", "tui/cli tooling", "embedded", "unix philosophy"},
+ Tools: []string{"zed (vim mode)", "opencode", "linux", "fish"},
+ School: "prospect high school (senior)",
+ Graduating: "may 2026",
+ }
+}
diff --git a/internal/config/experience.go b/internal/config/experience.go
@@ -0,0 +1,106 @@
+package config
+
+import (
+ "time"
+)
+
+type Experience struct {
+ ID string
+ Company string
+ Role string
+ StartDate time.Time
+ EndDate *time.Time
+ Current bool
+ Description []string
+ Tech []string
+ URL string
+ Logo string
+ LogoFilter string
+ Images []string
+}
+
+func GetExperience() []Experience {
+ return []Experience{
+ {
+ ID: "hark",
+ Company: "hark",
+ Role: "swe intern",
+ StartDate: date("2026-02"),
+ Current: true,
+ Description: []string{
+ "Hark is building vertically integrated AI systems across models, infrastructure, and hardware, focused on proactive, context-aware intelligence that moves beyond prompt-driven interfaces.",
+ },
+ URL: "https://hark.com",
+ Logo: "/static/hark/logo.svg",
+ LogoFilter: "logo-invert logo-hark",
+ },
+ {
+ ID: "mandolin",
+ Company: "mandolin",
+ Role: "senior swe intern",
+ StartDate: date("2024-10"),
+ EndDate: datePtr("2026-02"),
+ Description: []string{
+ "Building AI agents for specialty drug access. Automating prior authorizations, benefits verification, and revenue cycle management.",
+ "Mandolin raised $40M to automate access to life-saving medicines, wiring AI labor into thousands of infusion centers, specialty pharmacies, and health systems.",
+ },
+ Tech: []string{"typescript", "temporal"},
+ URL: "https://mandolin.com",
+ Logo: "/static/mandolin/logo.svg",
+ Images: []string{
+ "/static/mandolin/company.jpeg",
+ "/static/mandolin/company_silly.jpeg",
+ "/static/mandolin/employee_polaroids.jpeg",
+ "/static/mandolin/moment_polaroids.jpeg",
+ },
+ },
+ {
+ ID: "amplifier-health",
+ Company: "amplifier health",
+ Role: "swe intern",
+ StartDate: date("2024-06"),
+ EndDate: datePtr("2024-08"),
+ Description: []string{
+ "Voice AI for early disease detection. Large Acoustic Model analyzing 1000+ voice biomarkers across 30+ health conditions.",
+ },
+ Tech: []string{"python", "pytorch"},
+ URL: "https://amplifierhealth.com",
+ Logo: "/static/amplifier/logo.svg",
+ LogoFilter: "logo-invert",
+ },
+ }
+}
+
+func GetCurrentJob() *Experience {
+ return CurrentJob(GetExperience())
+}
+
+func CurrentJob(experience []Experience) *Experience {
+ for i := range experience {
+ if experience[i].Current {
+ return &experience[i]
+ }
+ }
+
+ return nil
+}
+
+func GetExperienceByID(id string) *Experience {
+ for _, exp := range GetExperience() {
+ if exp.ID == id {
+ return &exp
+ }
+ }
+
+ return nil
+}
+
+func date(s string) time.Time {
+ t, _ := time.Parse("2006-01", s)
+ return t
+}
+
+func datePtr(s string) *time.Time {
+ t := date(s)
+ return &t
+}
diff --git a/internal/config/projects.go b/internal/config/projects.go
@@ -0,0 +1,56 @@
+package config
+
+type Project struct {
+ ID string
+ Name string
+ Subtitle string
+ Tech []string
+ URL string
+ APIURL string
+}
+
+func GetProjects() []Project {
+ return []Project{
+ {
+ ID: "janitor",
+ Name: "janitor",
+ Subtitle: "audio classification with ast",
+ Tech: []string{"rust", "cuda"},
+ URL: "https://git.prime8.dev/moncheeta/janitor",
+ APIURL: "https://git.prime8.dev/api/v4/projects/moncheeta%2Fjanitor",
+ },
+ {
+ ID: "summarizer",
+ Name: "summarizer",
+ Subtitle: "audio transcription microservices",
+ Tech: []string{"python", "docker", "kafka"},
+ URL: "https://git.prime8.dev/moncheeta/summarizer",
+ APIURL: "https://git.prime8.dev/api/v4/projects/moncheeta%2Fsummarizer",
+ },
+ {
+ ID: "finger",
+ Name: "finger",
+ Subtitle: "mobile auth app",
+ Tech: []string{"typescript", "react native", "supabase"},
+ URL: "https://git.prime8.dev/moncheeta/finger",
+ APIURL: "https://git.prime8.dev/api/v4/projects/moncheeta%2Ffinger",
+ },
+ {
+ ID: "computer-science-club",
+ Name: "computer-science-club",
+ Subtitle: "cs club website",
+ Tech: []string{"python", "rest"},
+ URL: "https://git.prime8.dev/moncheeta/computer-science-club",
+ APIURL: "https://git.prime8.dev/api/v4/projects/moncheeta%2Fcomputer-science-club",
+ },
+ }
+}
+
+func GetProjectByID(id string) *Project {
+ for _, p := range GetProjects() {
+ if p.ID == id {
+ return &p
+ }
+ }
+ return nil
+}
diff --git a/internal/config/site.go b/internal/config/site.go
@@ -0,0 +1,6 @@
+package config
+
+const (
+ SiteDomain = "www.prime8.dev"
+ SiteURL = "https://" + SiteDomain
+)
diff --git a/internal/content/blogs.go b/internal/content/blogs.go
@@ -0,0 +1,148 @@
+package content
+
+import (
+ "errors"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "gopkg.in/yaml.v3"
+
+ "git.prime8.dev/moncheeta/prime8dev/internal/utils"
+)
+
+type Blog struct {
+ ID string
+ Title string
+ Description string
+ Date time.Time
+ Content string
+ URL string
+ Image string
+ Tags []string
+}
+
+type frontmatter struct {
+ ID string `yaml:"id"`
+ Title string `yaml:"title"`
+ Description string `yaml:"description"`
+ Date string `yaml:"date"`
+ URL string `yaml:"url"`
+ Image string `yaml:"image"`
+ Tags []string `yaml:"tags"`
+}
+
+var (
+ blogsOnce sync.Once
+ blogsCache []Blog
+ blogsByID map[string]Blog
+ blogsErr error
+)
+
+func InitBlogs() error {
+ _, err := loadBlogsCache()
+ return err
+}
+
+func LoadBlogs() ([]Blog, error) {
+ blogs, err := loadBlogsCache()
+ if err != nil {
+ return nil, err
+ }
+
+ copyBlogs := make([]Blog, len(blogs))
+ copy(copyBlogs, blogs)
+ return copyBlogs, nil
+}
+
+func loadBlogsCache() ([]Blog, error) {
+ blogsOnce.Do(func() {
+ paths, err := filepath.Glob("./content/blogs/*.md")
+ if err != nil {
+ blogsErr = err
+ return
+ }
+
+ blogs := make([]Blog, 0, len(paths))
+ for _, p := range paths {
+ b, err := parseBlogFile(p)
+ if err != nil {
+ blogsErr = err
+ return
+ }
+ blogs = append(blogs, b)
+ }
+
+ sort.Slice(blogs, func(i, j int) bool {
+ return blogs[i].Date.After(blogs[j].Date)
+ })
+
+ blogsCache = blogs
+ blogsByID = make(map[string]Blog, len(blogs))
+ for _, b := range blogs {
+ blogsByID[b.ID] = b
+ }
+ })
+
+ if blogsErr != nil {
+ return nil, blogsErr
+ }
+
+ return blogsCache, nil
+}
+
+func GetBlogByID(id string) (*Blog, error) {
+ _, err := loadBlogsCache()
+ if err != nil {
+ return nil, err
+ }
+
+ b, ok := blogsByID[id]
+ if !ok {
+ return nil, nil
+ }
+
+ copy := b
+ return ©, nil
+}
+
+func parseBlogFile(path string) (Blog, error) {
+ raw, err := os.ReadFile(path)
+ if err != nil {
+ return Blog{}, err
+ }
+
+ parts := strings.SplitN(string(raw), "---", 3)
+ if len(parts) < 3 {
+ return Blog{}, errors.New("invalid blog frontmatter format")
+ }
+
+ var fm frontmatter
+ if err := yaml.Unmarshal([]byte(parts[1]), &fm); err != nil {
+ return Blog{}, err
+ }
+
+ date, err := time.Parse("2006-01-02", fm.Date)
+ if err != nil {
+ return Blog{}, err
+ }
+
+ html, err := utils.MarkdownToHTML(parts[2])
+ if err != nil {
+ return Blog{}, err
+ }
+
+ return Blog{
+ ID: fm.ID,
+ Title: fm.Title,
+ Description: fm.Description,
+ Date: date,
+ Content: html,
+ URL: fm.URL,
+ Image: fm.Image,
+ Tags: fm.Tags,
+ }, nil
+}
diff --git a/internal/external/gitlab.go b/internal/external/gitlab.go
@@ -0,0 +1,162 @@
+package external
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "regexp"
+ "strings"
+ "time"
+
+ "git.prime8.dev/moncheeta/prime8dev/internal/config"
+ "git.prime8.dev/moncheeta/prime8dev/internal/utils"
+ "github.com/patrickmn/go-cache"
+)
+
+type GitLab struct {
+ cache *cache.Cache
+ httpClient *http.Client
+}
+
+const gitLabMetadataTTL = 15 * time.Minute
+
+func NewGitLab(cache *cache.Cache) *GitLab {
+ return &GitLab{
+ cache: cache,
+ httpClient: &http.Client{
+ Timeout: 10 * time.Second,
+ },
+ }
+}
+
+type GitMetadata struct {
+ Readme string
+ DateCreated time.Time
+ DateModified time.Time
+}
+
+func (s *GitLab) FetchMetadata(project config.Project) (*GitMetadata, error) {
+ cacheKey := "gitlab:" + project.ID
+
+ if val, found := s.cache.Get(cacheKey); found {
+ return val.(*GitMetadata), nil
+ }
+
+ metadata, err := s.fetchFromGitLab(project)
+ if err != nil {
+ return nil, err
+ }
+
+ s.cache.Set(cacheKey, metadata, gitLabMetadataTTL)
+ return metadata, nil
+}
+
+func (s *GitLab) RefreshMetadata(project config.Project) error {
+ metadata, err := s.fetchFromGitLab(project)
+ if err != nil {
+ return err
+ }
+
+ s.cache.Set("gitlab:"+project.ID, metadata, gitLabMetadataTTL)
+ return nil
+}
+
+func (s *GitLab) fetchFromGitLab(project config.Project) (*GitMetadata, error) {
+ req, err := http.NewRequest("GET", project.APIURL, nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("User-Agent", "prime8dev-portfolio")
+
+ resp, err := s.httpClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("gitlab API returned %d", resp.StatusCode)
+ }
+
+ var repo struct {
+ WebURL string `json:"web_url"`
+ DefaultBranch string `json:"default_branch"`
+ CreatedAt string `json:"created_at"`
+ LastActivityAt string `json:"last_activity_at"`
+ ReadmeURL string `json:"readme_url"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&repo); err != nil {
+ return nil, err
+ }
+
+ webURL := utils.EnsureHTTPS(repo.WebURL)
+
+ dateCreated, _ := time.Parse(time.RFC3339, repo.CreatedAt)
+ dateModified, _ := time.Parse(time.RFC3339, repo.LastActivityAt)
+
+ readme := ""
+ if repo.ReadmeURL != "" {
+ readmeURL := strings.Replace(utils.EnsureHTTPS(repo.ReadmeURL), "/-/blob/", "/-/raw/", 1)
+ readme = s.fetchReadme(webURL, repo.DefaultBranch, readmeURL)
+ }
+
+ return &GitMetadata{
+ Readme: readme,
+ DateCreated: dateCreated,
+ DateModified: dateModified,
+ }, nil
+}
+
+func (s *GitLab) fetchReadme(webURL, branch, readmeURL string) string {
+ resp, err := s.httpClient.Get(readmeURL)
+ if err != nil {
+ return ""
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return ""
+ }
+
+ contentBytes, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return ""
+ }
+ content := string(contentBytes)
+
+ return fixRelativePaths(webURL, branch, content)
+}
+
+var linkRegex = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
+var imageRegex = regexp.MustCompile(`!\[([^\]]*)\]\(([^)]+)\)`)
+
+func fixRelativePaths(webURL, branch, markdown string) string {
+ blob := fmt.Sprintf("%s/-/blob/%s", webURL, branch)
+ raw := fmt.Sprintf("%s/-/raw/%s", webURL, branch)
+
+ markdown = imageRegex.ReplaceAllStringFunc(markdown, func(match string) string {
+ parts := imageRegex.FindStringSubmatch(match)
+ if len(parts) != 3 {
+ return match
+ }
+ alt, path := parts[1], parts[2]
+ if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
+ return match
+ }
+ return fmt.Sprintf("", alt, raw, path)
+ })
+
+ markdown = linkRegex.ReplaceAllStringFunc(markdown, func(match string) string {
+ parts := linkRegex.FindStringSubmatch(match)
+ if len(parts) != 3 {
+ return match
+ }
+ text, path := parts[1], parts[2]
+ if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
+ return match
+ }
+ return fmt.Sprintf("[%s](%s/%s)", text, blob, path)
+ })
+ return markdown
+}
diff --git a/internal/external/wakatime.go b/internal/external/wakatime.go
@@ -0,0 +1,282 @@
+package external
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "time"
+
+ "github.com/patrickmn/go-cache"
+)
+
+type WakaTime struct {
+ httpClient *http.Client
+ apiKey string
+ baseURL string
+ cache *cache.Cache
+}
+
+func NewWakaTime(cache *cache.Cache) *WakaTime {
+ apiKey := os.Getenv("WAKATIME_API_KEY")
+
+ return &WakaTime{
+ httpClient: &http.Client{
+ Timeout: 10 * time.Second,
+ },
+ apiKey: apiKey,
+ baseURL: "https://wakatime.com/api/v1",
+ cache: cache,
+ }
+}
+
+type WakaTimeStats struct {
+ AllTime AllTimeStats
+ Weekly WeeklyStats
+}
+
+func (s *WakaTime) FetchStats() (*WakaTimeStats, error) {
+ if s.apiKey == "" {
+ return nil, nil
+ }
+
+ cacheKey := "wakatime:stats"
+
+ if val, found := s.cache.Get(cacheKey); found {
+ return val.(*WakaTimeStats), nil
+ }
+
+ allTime, err := s.fetchAllTimeStats()
+ if err != nil {
+ return nil, err
+ }
+
+ weekly, err := s.fetchWeeklyStats()
+ if err != nil {
+ return nil, err
+ }
+
+ stats := &WakaTimeStats{
+ AllTime: *allTime,
+ Weekly: *weekly,
+ }
+
+ s.cache.Set(cacheKey, stats, 5*time.Minute)
+ return stats, nil
+}
+
+type allTimeResponse struct {
+ Data struct {
+ TotalSeconds float64 `json:"total_seconds"`
+ } `json:"data"`
+}
+
+type languagesResponse struct {
+ Data struct {
+ DailyAverageIncludingOtherLanguage float64 `json:"daily_average_including_other_language"`
+ } `json:"data"`
+}
+
+type AllTimeStats struct {
+ TotalText string
+ DailyAverageText string
+}
+
+func (s *WakaTime) fetchAllTimeStats() (*AllTimeStats, error) {
+ cacheKey := "wakatime:alltime"
+
+ if val, found := s.cache.Get(cacheKey); found {
+ return val.(*AllTimeStats), nil
+ }
+
+ statsCh := make(chan *allTimeResponse, 1)
+ langCh := make(chan *languagesResponse, 1)
+ errCh := make(chan error, 2)
+
+ go func() {
+ resp, err := s.makeRequest("/users/current/all_time_since_today")
+ if err != nil {
+ errCh <- err
+ return
+ }
+ var data allTimeResponse
+ if err := json.Unmarshal(resp, &data); err != nil {
+ errCh <- err
+ return
+ }
+ statsCh <- &data
+ }()
+
+ go func() {
+ resp, err := s.makeRequest("/users/current/stats/last_year")
+ if err != nil {
+ errCh <- err
+ return
+ }
+ var data languagesResponse
+ if err := json.Unmarshal(resp, &data); err != nil {
+ errCh <- err
+ return
+ }
+ langCh <- &data
+ }()
+
+ var stats *allTimeResponse
+ var langStats *languagesResponse
+
+ for range 2 {
+ select {
+ case s := <-statsCh:
+ stats = s
+ case l := <-langCh:
+ langStats = l
+ case err := <-errCh:
+ return nil, err
+ }
+ }
+
+ totalSeconds := 0.0
+ if stats != nil {
+ totalSeconds = stats.Data.TotalSeconds
+ }
+ totalHours := int(totalSeconds / 3600)
+ totalText := fmt.Sprintf("%d+ hrs coded", totalHours)
+
+ dailyAvgText := "0 mins"
+ if langStats != nil && langStats.Data.DailyAverageIncludingOtherLanguage > 0 {
+ dailyAvgText = formatDuration(langStats.Data.DailyAverageIncludingOtherLanguage)
+ }
+
+ result := &AllTimeStats{
+ TotalText: totalText,
+ DailyAverageText: dailyAvgText,
+ }
+
+ s.cache.Set(cacheKey, result, 5*time.Minute)
+ return result, nil
+}
+
+func formatDuration(seconds float64) string {
+ hours := int(seconds / 3600)
+ minutes := int((seconds - float64(hours)*3600) / 60)
+
+ if hours > 0 {
+ return fmt.Sprintf("%d hrs %d mins", hours, minutes)
+ }
+ return fmt.Sprintf("%d mins", minutes)
+}
+
+type summariesResponse struct {
+ Data []struct {
+ Range struct {
+ Date string `json:"date"`
+ } `json:"range"`
+ GrandTotal struct {
+ TotalSeconds float64 `json:"total_seconds"`
+ } `json:"grand_total"`
+ } `json:"data"`
+}
+
+type WeeklyStats struct {
+ TotalText string
+ Days []DailyEntry
+}
+
+type DailyEntry struct {
+ DayText string
+ DateText string
+ TimeText string
+ BarWidthPercent string
+}
+
+func (s *WakaTime) fetchWeeklyStats() (*WeeklyStats, error) {
+ cacheKey := "wakatime:weekly"
+
+ if val, found := s.cache.Get(cacheKey); found {
+ return val.(*WeeklyStats), nil
+ }
+
+ end := time.Now()
+ start := end.AddDate(0, 0, -6)
+
+ url := fmt.Sprintf("/users/current/summaries?start=%s&end=%s",
+ start.Format("2006-01-02"),
+ end.Format("2006-01-02"))
+
+ resp, err := s.makeRequest(url)
+ if err != nil {
+ return nil, err
+ }
+
+ var data summariesResponse
+ if err := json.Unmarshal(resp, &data); err != nil {
+ return nil, err
+ }
+
+ days := make([]DailyEntry, 0, len(data.Data))
+ var totalSeconds float64
+ var maxMinutes = 1
+ minutesPerDay := make([]int, len(data.Data))
+
+ for i, day := range data.Data {
+ date, _ := time.Parse("2006-01-02", day.Range.Date)
+
+ dayText := date.Weekday().String()[:3]
+ dateText := fmt.Sprintf("%s %d", date.Month().String()[:3], date.Day())
+
+ seconds := day.GrandTotal.TotalSeconds
+ totalSeconds += seconds
+
+ minutes := int(seconds / 60)
+ minutesPerDay[i] = minutes
+ if minutes > maxMinutes {
+ maxMinutes = minutes
+ }
+
+ timeText := formatDuration(seconds)
+
+ days = append(days, DailyEntry{
+ DayText: dayText,
+ DateText: dateText,
+ TimeText: timeText,
+ })
+ }
+
+ for i := range days {
+ days[i].BarWidthPercent = fmt.Sprintf("%d%%", (minutesPerDay[i]*100)/maxMinutes)
+ }
+
+ result := &WeeklyStats{
+ TotalText: formatDuration(totalSeconds),
+ Days: days,
+ }
+
+ s.cache.Set(cacheKey, result, 5*time.Minute)
+ return result, nil
+}
+
+func (s *WakaTime) makeRequest(path string) ([]byte, error) {
+ url := s.baseURL + path
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ auth := base64.StdEncoding.EncodeToString([]byte(s.apiKey))
+ req.Header.Set("Authorization", fmt.Sprintf("Basic %s", auth))
+
+ resp, err := s.httpClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("wakatime API returned %d", resp.StatusCode)
+ }
+
+ return io.ReadAll(resp.Body)
+}
diff --git a/internal/handlers/blog.go b/internal/handlers/blog.go
@@ -0,0 +1,74 @@
+package handlers
+
+import (
+ "net/http"
+
+ "git.prime8.dev/moncheeta/prime8dev/internal/config"
+ "git.prime8.dev/moncheeta/prime8dev/internal/content"
+ "git.prime8.dev/moncheeta/prime8dev/internal/utils"
+ "git.prime8.dev/moncheeta/prime8dev/internal/view"
+ "github.com/go-chi/chi/v5"
+)
+
+func (h *Handler) BlogListHandler(w http.ResponseWriter, r *http.Request) {
+ about := config.GetAbout()
+ blogs, err := content.LoadBlogs()
+ if err != nil {
+ http.Error(w, "Failed to load blogs", http.StatusInternalServerError)
+ return
+ }
+
+ data := view.BaseData("Blog", "blog", about)
+ data.Blogs = blogs
+
+ if utils.IsHTMXRequest(r) {
+ h.renderTemplate(w, r, "blog", data, fragmentCacheMaxAgeSeconds)
+ return
+ }
+
+ h.renderTemplate(w, r, "layout", data, fullPageCacheMaxAgeSeconds)
+}
+
+func (h *Handler) BlogPageHandler(w http.ResponseWriter, r *http.Request) {
+ id := chi.URLParam(r, "id")
+
+ about := config.GetAbout()
+ blogs, err := content.LoadBlogs()
+ if err != nil {
+ http.Error(w, "Failed to load blogs", http.StatusInternalServerError)
+ return
+ }
+
+ blog, err := content.GetBlogByID(id)
+ if err != nil {
+ http.Error(w, "Failed to load blog", http.StatusInternalServerError)
+ return
+ }
+ if blog == nil {
+ http.NotFound(w, r)
+ return
+ }
+
+ data := view.BaseData("Blog", "blog", about)
+ data.Blogs = blogs
+ data.SelectedBlogID = id
+ data.Blog = blog
+
+ h.renderTemplate(w, r, "layout", data, fullPageCacheMaxAgeSeconds)
+}
+
+func (h *Handler) BlogDetailHandler(w http.ResponseWriter, r *http.Request) {
+ id := chi.URLParam(r, "id")
+ blog, err := content.GetBlogByID(id)
+ if err != nil {
+ http.Error(w, "Failed to load blog", http.StatusInternalServerError)
+ return
+ }
+ if blog == nil {
+ http.Error(w, "Blog not found", http.StatusNotFound)
+ return
+ }
+
+ data := view.Data{Blog: blog}
+ h.renderTemplate(w, r, "blog_detail", data, fragmentCacheMaxAgeSeconds)
+}
diff --git a/internal/handlers/cache.go b/internal/handlers/cache.go
@@ -0,0 +1,6 @@
+package handlers
+
+const (
+ fragmentCacheMaxAgeSeconds = 5 * 60
+ fullPageCacheMaxAgeSeconds = 60 * 60
+)
diff --git a/internal/handlers/experience.go b/internal/handlers/experience.go
@@ -0,0 +1,46 @@
+package handlers
+
+import (
+ "net/http"
+
+ "git.prime8.dev/moncheeta/prime8dev/internal/config"
+ "git.prime8.dev/moncheeta/prime8dev/internal/utils"
+ "git.prime8.dev/moncheeta/prime8dev/internal/view"
+ "github.com/go-chi/chi/v5"
+)
+
+func (h *Handler) ExperienceListHandler(w http.ResponseWriter, r *http.Request) {
+ about := config.GetAbout()
+ experience := config.GetExperience()
+ data := view.BaseData("Experience", "experience", about)
+ data.Experience = experience
+
+ if utils.IsHTMXRequest(r) {
+ h.renderTemplate(w, r, "experience", data, fragmentCacheMaxAgeSeconds)
+ return
+ }
+
+ h.renderTemplate(w, r, "layout", data, fullPageCacheMaxAgeSeconds)
+}
+
+func (h *Handler) ExperiencePageHandler(w http.ResponseWriter, r *http.Request) {
+ id := chi.URLParam(r, "id")
+ exp := config.GetExperienceByID(id)
+ if exp == nil {
+ http.NotFound(w, r)
+ return
+ }
+
+ about := config.GetAbout()
+ experience := config.GetExperience()
+ data := view.BaseData("Experience", "experience", about)
+ data.Experience = experience
+ data.SelectedExperienceID = id
+
+ if utils.IsHTMXRequest(r) {
+ h.renderTemplate(w, r, "experience", data, fragmentCacheMaxAgeSeconds)
+ return
+ }
+
+ h.renderTemplate(w, r, "layout", data, fullPageCacheMaxAgeSeconds)
+}
diff --git a/internal/handlers/handler.go b/internal/handlers/handler.go
@@ -0,0 +1,57 @@
+package handlers
+
+import (
+ "log"
+ "time"
+
+ "git.prime8.dev/moncheeta/prime8dev/internal/config"
+ "git.prime8.dev/moncheeta/prime8dev/internal/external"
+ "git.prime8.dev/moncheeta/prime8dev/internal/view"
+ "github.com/patrickmn/go-cache"
+)
+
+type Handler struct {
+ cache *cache.Cache
+ gitLabService *external.GitLab
+ wakaTimeService *external.WakaTime
+ renderer *view.Renderer
+}
+
+func NewHandler() *Handler {
+ c := cache.New(5*time.Minute, 10*time.Minute)
+ renderer, err := view.New()
+ if err != nil {
+ log.Fatalf("failed to initialize templates: %v", err)
+ }
+
+ h := &Handler{
+ cache: c,
+ gitLabService: external.NewGitLab(c),
+ wakaTimeService: external.NewWakaTime(c),
+ renderer: renderer,
+ }
+
+ h.startProjectMetadataRefresh()
+ return h
+}
+
+func (h *Handler) startProjectMetadataRefresh() {
+ go func() {
+ h.refreshProjectMetadata()
+
+ ticker := time.NewTicker(5 * time.Minute)
+ defer ticker.Stop()
+
+ for range ticker.C {
+ h.refreshProjectMetadata()
+ }
+ }()
+}
+
+func (h *Handler) refreshProjectMetadata() {
+ for _, project := range config.GetProjects() {
+ if err := h.gitLabService.RefreshMetadata(project); err != nil {
+ log.Printf("project metadata refresh failed for %s: %v", project.ID, err)
+ }
+ }
+}
diff --git a/internal/handlers/home.go b/internal/handlers/home.go
@@ -0,0 +1,24 @@
+package handlers
+
+import (
+ "net/http"
+
+ "git.prime8.dev/moncheeta/prime8dev/internal/config"
+ "git.prime8.dev/moncheeta/prime8dev/internal/utils"
+ "git.prime8.dev/moncheeta/prime8dev/internal/view"
+)
+
+func (h *Handler) HomeHandler(w http.ResponseWriter, r *http.Request) {
+ about := config.GetAbout()
+ experience := config.GetExperience()
+ data := view.BaseData("About", "about", about)
+ data.Experience = experience
+ data.CurrentJob = config.CurrentJob(experience)
+
+ if utils.IsHTMXRequest(r) {
+ h.renderTemplate(w, r, "about", data, fragmentCacheMaxAgeSeconds)
+ return
+ }
+
+ h.renderTemplate(w, r, "layout", data, fullPageCacheMaxAgeSeconds)
+}
diff --git a/internal/handlers/projects.go b/internal/handlers/projects.go
@@ -0,0 +1,91 @@
+package handlers
+
+import (
+ "html/template"
+ "net/http"
+ "strings"
+
+ "git.prime8.dev/moncheeta/prime8dev/internal/config"
+ "git.prime8.dev/moncheeta/prime8dev/internal/utils"
+ "git.prime8.dev/moncheeta/prime8dev/internal/view"
+ "github.com/go-chi/chi/v5"
+)
+
+func (h *Handler) ProjectsListHandler(w http.ResponseWriter, r *http.Request) {
+ about := config.GetAbout()
+ projects := config.GetProjects()
+ data := view.BaseData("Projects", "projects", about)
+ data.Projects = projects
+
+ if utils.IsHTMXRequest(r) {
+ h.renderTemplate(w, r, "projects", data, fragmentCacheMaxAgeSeconds)
+ return
+ }
+
+ h.renderTemplate(w, r, "layout", data, fullPageCacheMaxAgeSeconds)
+}
+
+func (h *Handler) ProjectPageHandler(w http.ResponseWriter, r *http.Request) {
+ id := chi.URLParam(r, "id")
+ project := config.GetProjectByID(id)
+ if project == nil {
+ http.NotFound(w, r)
+ return
+ }
+
+ about := config.GetAbout()
+ projects := config.GetProjects()
+ data := view.BaseData("Projects", "projects", about)
+ data.Projects = projects
+ data.SelectedProjectID = id
+ data.Project = project
+ data.ProjectLinkLabel = projectLinkLabel(project.URL)
+
+ metadata, err := h.gitLabService.FetchMetadata(*project)
+ if err == nil && metadata != nil && metadata.Readme != "" {
+ html, mdErr := utils.MarkdownToHTML(metadata.Readme)
+ if mdErr == nil {
+ data.ProjectReadme = template.HTML(html)
+ }
+ }
+
+ h.renderTemplate(w, r, "layout", data, fullPageCacheMaxAgeSeconds)
+}
+
+func (h *Handler) ProjectDetailHandler(w http.ResponseWriter, r *http.Request) {
+ id := chi.URLParam(r, "id")
+ project := config.GetProjectByID(id)
+ if project == nil {
+ http.Error(w, "Project not found", http.StatusNotFound)
+ return
+ }
+
+ metadata, err := h.gitLabService.FetchMetadata(*project)
+ if err != nil {
+ metadata = nil
+ }
+
+ data := view.Data{
+ Project: project,
+ ProjectLinkLabel: projectLinkLabel(project.URL),
+ }
+
+ if metadata != nil && metadata.Readme != "" {
+ html, err := utils.MarkdownToHTML(metadata.Readme)
+ if err == nil {
+ data.ProjectReadme = template.HTML(html)
+ }
+ }
+
+ h.renderTemplate(w, r, "project_detail", data, fragmentCacheMaxAgeSeconds)
+}
+
+func projectLinkLabel(raw string) string {
+ if strings.Contains(raw, "git.prime8.dev") || strings.Contains(raw, "gitlab.com") {
+ return "GitLab"
+ } else if strings.Contains(raw, "github.com") {
+ return "GitHub"
+ } else {
+ return "Outside"
+ }
+}
diff --git a/internal/handlers/render.go b/internal/handlers/render.go
@@ -0,0 +1,54 @@
+package handlers
+
+import (
+ "crypto/sha256"
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+
+ "git.prime8.dev/moncheeta/prime8dev/internal/view"
+)
+
+func (h *Handler) renderTemplate(w http.ResponseWriter, r *http.Request, name string, data view.Data, maxAgeSeconds int) {
+ if maxAgeSeconds < 0 {
+ maxAgeSeconds = 0
+ }
+
+ body, err := h.renderer.Render(name, data)
+ if err != nil {
+ http.Error(w, "Failed to render content", http.StatusInternalServerError)
+ return
+ }
+
+ sum := sha256.Sum256([]byte(body))
+ etag := fmt.Sprintf(`W/"%x"`, sum)
+ cacheEnabled := shouldCacheResponses()
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ if !cacheEnabled {
+ applyNoCacheHeaders(w)
+ } else if maxAgeSeconds > 0 {
+ w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAgeSeconds))
+ w.Header().Set("ETag", etag)
+ }
+ w.Header().Set("Vary", "HX-Request")
+
+ if cacheEnabled && maxAgeSeconds > 0 && strings.Contains(r.Header.Get("If-None-Match"), etag) {
+ w.WriteHeader(http.StatusNotModified)
+ return
+ }
+
+ _, _ = w.Write([]byte(body))
+}
+
+func shouldCacheResponses() bool {
+ mode := strings.ToLower(strings.TrimSpace(os.Getenv("APP_ENV")))
+ return mode == "prod"
+}
+
+func applyNoCacheHeaders(w http.ResponseWriter) {
+ w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
+ w.Header().Set("Pragma", "no-cache")
+ w.Header().Set("Expires", "0")
+}
diff --git a/internal/handlers/sitemap.go b/internal/handlers/sitemap.go
@@ -0,0 +1,42 @@
+package handlers
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+
+ "git.prime8.dev/moncheeta/prime8dev/internal/config"
+ "git.prime8.dev/moncheeta/prime8dev/internal/content"
+)
+
+func (h *Handler) SitemapHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/xml")
+
+ var xml bytes.Buffer
+ xml.WriteString(`<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`)
+
+ blogs, _ := content.LoadBlogs()
+ projects := config.GetProjects()
+ experience := config.GetExperience()
+
+ routes := []string{"/", "/projects", "/experience", "/blog"}
+ for _, route := range routes {
+ fmt.Fprintf(&xml, "<url><loc>%s%s</loc></url>", config.SiteURL, route)
+ }
+
+ for _, p := range projects {
+ fmt.Fprintf(&xml, "<url><loc>%s/projects/%s</loc></url>", config.SiteURL, p.ID)
+ }
+
+ for _, exp := range experience {
+ fmt.Fprintf(&xml, "<url><loc>%s/experience/%s</loc></url>", config.SiteURL, exp.ID)
+ }
+
+ for _, b := range blogs {
+ fmt.Fprintf(&xml, "<url><loc>%s/blog/%s</loc></url>", config.SiteURL, b.ID)
+ }
+
+ xml.WriteString("</urlset>")
+ _, _ = w.Write(xml.Bytes())
+}
diff --git a/internal/handlers/wakatime.go b/internal/handlers/wakatime.go
@@ -0,0 +1,24 @@
+package handlers
+
+import (
+ "encoding/json"
+ "net/http"
+)
+
+func (h *Handler) WakaTimeHandler(w http.ResponseWriter, r *http.Request) {
+ stats, err := h.wakaTimeService.FetchStats()
+ if err != nil {
+ http.Error(w, "Failed to fetch stats", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ if stats == nil {
+ _, _ = w.Write([]byte("null"))
+ return
+ }
+
+ if err := json.NewEncoder(w).Encode(stats); err != nil {
+ http.Error(w, "Failed to encode stats", http.StatusInternalServerError)
+ }
+}
diff --git a/internal/utils/htmx.go b/internal/utils/htmx.go
@@ -0,0 +1,7 @@
+package utils
+
+import "net/http"
+
+func IsHTMXRequest(r *http.Request) bool {
+ return r.Header.Get("HX-Request") == "true"
+}
diff --git a/internal/utils/markdown.go b/internal/utils/markdown.go
@@ -0,0 +1,63 @@
+package utils
+
+import (
+ "bytes"
+ "html/template"
+ "strings"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/extension"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer/html"
+)
+
+var TemplateFuncs = template.FuncMap{
+ "markdown": func(content string) template.HTML {
+ html, err := MarkdownToHTML(content)
+ if err != nil {
+ return template.HTML(content)
+ }
+ return template.HTML(html)
+ },
+ "contains": strings.Contains,
+}
+
+func MarkdownToHTML(markdown string) (string, error) {
+ md := goldmark.New(
+ goldmark.WithExtensions(
+ extension.GFM,
+ extension.Typographer,
+ ),
+ goldmark.WithParserOptions(
+ parser.WithAutoHeadingID(),
+ ),
+ goldmark.WithRendererOptions(
+ html.WithHardWraps(),
+ html.WithXHTML(),
+ ),
+ )
+
+ var buf bytes.Buffer
+ if err := md.Convert([]byte(RemoveFirstH1(markdown)), &buf); err != nil {
+ return "", err
+ }
+
+ return buf.String(), nil
+}
+
+func RemoveFirstH1(markdown string) string {
+ lines := strings.Split(markdown, "\n")
+ var result []string
+ skippedFirstH1 := false
+
+ for _, line := range lines {
+ trimmed := strings.TrimSpace(line)
+ if !skippedFirstH1 && strings.HasPrefix(trimmed, "# ") {
+ skippedFirstH1 = true
+ continue
+ }
+ result = append(result, line)
+ }
+
+ return strings.Join(result, "\n")
+}
diff --git a/internal/utils/period.go b/internal/utils/period.go
@@ -0,0 +1,97 @@
+package utils
+
+import (
+ "fmt"
+ "strings"
+ "time"
+)
+
+var monthNames = []string{
+ "january",
+ "february",
+ "march",
+ "april",
+ "may",
+ "june",
+ "july",
+ "august",
+ "september",
+ "october",
+ "november",
+ "december",
+}
+
+func FormatMonthYear(t time.Time) string {
+ month := int(t.Month()) - 1
+ if month < 0 || month >= len(monthNames) {
+ return strings.ToLower(t.Format("Jan 2006"))
+ }
+
+ return fmt.Sprintf("%s %s", monthNames[month], t.Format("2006"))
+}
+
+func FormatEndDate(endDate *time.Time, current bool) string {
+ if current || endDate == nil {
+ return "present"
+ }
+
+ return FormatMonthYear(*endDate)
+}
+
+func MonthsDuration(startDate time.Time, endDate *time.Time, current bool) int {
+ end := time.Now()
+ if !current && endDate != nil {
+ end = *endDate
+ }
+
+ months := (end.Year()-startDate.Year())*12 + int(end.Month()-startDate.Month()) + 1
+ if months < 1 {
+ return 1
+ }
+
+ return months
+}
+
+func MonthWord(months int) string {
+ if months == 1 {
+ return "month"
+ }
+
+ return "months"
+}
+
+func FormatDuration(startDate time.Time, endDate *time.Time, current bool) string {
+ months := MonthsDuration(startDate, endDate, current)
+ years := months / 12
+ remainingMonths := months % 12
+
+ if years == 0 {
+ return fmt.Sprintf("%d %s", months, MonthWord(months))
+ }
+
+ if remainingMonths == 0 {
+ return fmt.Sprintf("%d %s", years, yearWord(years))
+ }
+
+ return fmt.Sprintf("%d %s %d %s", years, yearWord(years), remainingMonths, MonthWord(remainingMonths))
+}
+
+func DurationHasYear(startDate time.Time, endDate *time.Time, current bool) bool {
+ return MonthsDuration(startDate, endDate, current) >= 12
+}
+
+func yearWord(years int) string {
+ if years == 1 {
+ return "year"
+ }
+
+ return "years"
+}
+
+func TimelineYear(endDate *time.Time, current bool) string {
+ if current || endDate == nil {
+ return "now"
+ }
+
+ return endDate.Format("2006")
+}
diff --git a/internal/utils/tab.go b/internal/utils/tab.go
@@ -0,0 +1,19 @@
+package utils
+
+import (
+ "fmt"
+
+ "git.prime8.dev/moncheeta/prime8dev/internal/config"
+)
+
+func TabPath(id string) string {
+ if id == "about" {
+ return "/"
+ }
+
+ return fmt.Sprintf("/%s", id)
+}
+
+func TabURL(id string) string {
+ return fmt.Sprintf("%s%s", config.SiteURL, TabPath(id))
+}
diff --git a/internal/utils/tags.go b/internal/utils/tags.go
@@ -0,0 +1,90 @@
+package utils
+
+import "strings"
+
+var tagClassMap = map[string]string{
+ // Languages
+ "Zig": "lang-zig",
+ "Rust": "lang-rust",
+ "C": "lang-c",
+ "Go": "lang-go",
+ "TypeScript": "lang-ts",
+ "Python": "lang-python",
+
+ // Tools
+ "Zed (Vim mode)": "tool-zed",
+ "Neovim": "tool-neovim",
+ "OpenCode": "tool-opencode",
+ "Linux": "tool-linux",
+ "Fish": "tool-fish",
+ "Ghostty": "tool-ghostty",
+
+ // Interests
+ "Low-level systems": "interest-lowlevel",
+ "TUI/CLI tooling": "interest-tui",
+ "Embedded": "interest-embedded",
+ "UNIX philosophy": "interest-unix",
+
+ // Tech
+ "Temporal": "tech-temporal",
+ "PostgreSQL": "tech-postgres",
+ "PyTorch": "tech-pytorch",
+ "CUDA": "tech-cuda",
+ "Docker": "tech-docker",
+ "Kafka": "tech-kafka",
+ "React Native": "tech-rn",
+ "Supabase": "tech-supabase",
+ "REST": "tech-rest",
+}
+
+var techAbbreviations = map[string]string{
+ "TypeScript": "TS",
+}
+
+func TagClass(tag string) string {
+ if className, ok := tagClassMap[tag]; ok {
+ return className
+ }
+
+ for knownTag, className := range tagClassMap {
+ if strings.EqualFold(knownTag, tag) {
+ return className
+ }
+ }
+
+ return ""
+}
+
+func TechMeta(tech []string) string {
+ if len(tech) == 0 {
+ return ""
+ }
+
+ parts := make([]string, 0, 2)
+ for i, item := range tech {
+ if i >= 2 {
+ break
+ }
+
+ if short, ok := techAbbreviations[item]; ok {
+ parts = append(parts, short)
+ continue
+ }
+
+ short := ""
+ for tech, abbreviation := range techAbbreviations {
+ if strings.EqualFold(tech, item) {
+ short = abbreviation
+ break
+ }
+ }
+
+ if short != "" {
+ parts = append(parts, short)
+ } else {
+ parts = append(parts, item)
+ }
+ }
+
+ return strings.Join(parts, ", ")
+}
diff --git a/internal/utils/url.go b/internal/utils/url.go
@@ -0,0 +1,7 @@
+package utils
+
+import "strings"
+
+func EnsureHTTPS(url string) string {
+ return strings.Replace(url, "http://", "https://", 1)
+}
diff --git a/internal/view/ascii.go b/internal/view/ascii.go
@@ -0,0 +1,34 @@
+package view
+
+import (
+ "strings"
+ "sync"
+
+ asciiart "github.com/romance-dev/ascii-art"
+ _ "github.com/romance-dev/ascii-art/fonts/doom"
+ _ "github.com/romance-dev/ascii-art/fonts/standard"
+)
+
+var (
+ aboutASCIIOnce sync.Once
+ aboutASCII string
+)
+
+func aboutNameASCII(name string) string {
+ aboutASCIIOnce.Do(func() {
+ defer func() {
+ if recover() != nil {
+ aboutASCII = name
+ }
+ }()
+
+ fig := asciiart.NewFigure(name, "doom", false)
+ aboutASCII = strings.TrimRight(fig.String(), "\n")
+ })
+
+ if aboutASCII == "" {
+ return name
+ }
+
+ return aboutASCII
+}
diff --git a/internal/view/renderer.go b/internal/view/renderer.go
@@ -0,0 +1,142 @@
+package view
+
+import (
+ "embed"
+ "fmt"
+ "html/template"
+ "io/fs"
+ "path"
+ "strings"
+
+ "git.prime8.dev/moncheeta/prime8dev/internal/config"
+ "git.prime8.dev/moncheeta/prime8dev/internal/content"
+ "git.prime8.dev/moncheeta/prime8dev/internal/external"
+ "git.prime8.dev/moncheeta/prime8dev/internal/utils"
+)
+
+//go:embed templates/*.gohtml templates/partials/*.gohtml styles/*.css
+var templatesFS embed.FS
+
+type Data struct {
+ Title string
+ Description string
+ ActiveTab string
+ ContentTemplate string
+ About config.About
+ AboutNameASCII string
+ Experience []config.Experience
+ SelectedExperienceID string
+ CurrentJob *config.Experience
+ Projects []config.Project
+ SelectedProjectID string
+ Project *config.Project
+ ProjectLinkLabel string
+ ProjectReadme template.HTML
+ Blogs []content.Blog
+ SelectedBlogID string
+ Blog *content.Blog
+ WakaStats *external.WakaTimeStats
+ Tabs []Tab
+ SiteURL string
+}
+
+type Tab struct {
+ ID string
+ Label string
+ Num int
+}
+
+type Renderer struct {
+ tpl *template.Template
+}
+
+func New() (*Renderer, error) {
+ inlineStyles, err := loadInlineStyles()
+ if err != nil {
+ return nil, err
+ }
+
+ funcs := template.FuncMap{
+ "tabPath": utils.TabPath,
+ "tabURL": utils.TabURL,
+ "tagClass": utils.TagClass,
+ "techMeta": utils.TechMeta,
+ "formatMonthYear": utils.FormatMonthYear,
+ "formatEndDate": utils.FormatEndDate,
+ "formatDuration": utils.FormatDuration,
+ "durationHasYear": utils.DurationHasYear,
+ "timelineYear": utils.TimelineYear,
+ "toHTML": toHTML,
+ "inlineCSS": func(name string) template.CSS {
+ if css, ok := inlineStyles[name]; ok {
+ return css
+ }
+ return ""
+ },
+ }
+
+ tpl, err := template.New("site").Funcs(funcs).ParseFS(templatesFS, "templates/*.gohtml", "templates/partials/*.gohtml")
+ if err != nil {
+ return nil, fmt.Errorf("parse templates: %w", err)
+ }
+
+ return &Renderer{tpl: tpl}, nil
+}
+
+func (r *Renderer) Render(name string, data Data) (string, error) {
+ var b strings.Builder
+ if err := r.tpl.ExecuteTemplate(&b, name, data); err != nil {
+ return "", err
+ }
+ return b.String(), nil
+}
+
+func DefaultTabs() []Tab {
+ return []Tab{
+ {ID: "about", Label: "About", Num: 1},
+ {ID: "projects", Label: "Projects", Num: 2},
+ {ID: "experience", Label: "Experience", Num: 3},
+ {ID: "blog", Label: "Blog", Num: 4},
+ }
+}
+
+func BaseData(title, activeTab string, about config.About) Data {
+ return Data{
+ Title: title,
+ Description: fmt.Sprintf("Portfolio and blog of %s", about.Name),
+ ActiveTab: activeTab,
+ ContentTemplate: activeTab,
+ About: about,
+ AboutNameASCII: aboutNameASCII(about.Name),
+ Tabs: DefaultTabs(),
+ SiteURL: config.SiteURL,
+ }
+}
+
+func toHTML(s string) template.HTML {
+ return template.HTML(s)
+}
+
+func loadInlineStyles() (map[string]template.CSS, error) {
+ entries, err := fs.ReadDir(templatesFS, "styles")
+ if err != nil {
+ return nil, fmt.Errorf("read styles: %w", err)
+ }
+
+ styles := make(map[string]template.CSS, len(entries))
+ for _, entry := range entries {
+ if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".css") {
+ continue
+ }
+
+ stylePath := path.Join("styles", entry.Name())
+ content, err := templatesFS.ReadFile(stylePath)
+ if err != nil {
+ return nil, fmt.Errorf("read style %s: %w", entry.Name(), err)
+ }
+
+ styles[entry.Name()] = template.CSS(string(content))
+ }
+
+ return styles, nil
+}
diff --git a/internal/view/scripts/app.js b/internal/view/scripts/app.js
@@ -0,0 +1,46 @@
+import { initAbout } from "./modules/about.js";
+import { initBlog } from "./modules/blog.js";
+import { initBlogDetail } from "./modules/blogDetail.js";
+import { initExperience } from "./modules/experience.js";
+import { initHeader, updateAppTitle } from "./modules/header.js";
+import { initHelp } from "./modules/help.js";
+import { initLayout } from "./modules/layout.js";
+import { initLightbox } from "./modules/lightbox.js";
+import { initProjects } from "./modules/projects.js";
+
+function initApp() {
+ initHeader();
+ initHelp();
+ initLightbox();
+ initLayout();
+
+ const initDynamicContent = () => {
+ initAbout();
+ initProjects();
+ initExperience();
+ initBlog();
+ initBlogDetail();
+ updateAppTitle();
+ };
+ initDynamicContent();
+
+ document.addEventListener("htmx:afterSwap", (e) => {
+ if (!e.target) return;
+
+ switch (e.target.id) {
+ case "content":
+ case "project-detail":
+ case "blog-post-content":
+ initDynamicContent();
+ break;
+ default:
+ break;
+ }
+ });
+}
+
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", initApp, { once: true });
+} else {
+ initApp();
+}
diff --git a/internal/view/scripts/modules/about.js b/internal/view/scripts/modules/about.js
@@ -0,0 +1,93 @@
+import { getWakaTimeData, subscribeWakaTimeRefresh } from "./wakatime.js";
+
+let unsubscribeAboutRefresh = null;
+
+export function initAbout() {
+ const statsContainer = document.getElementById("activity-stats");
+ const section = document.getElementById("activity-section");
+ if (!statsContainer || !section) return;
+
+ function hideActivity() {
+ section.style.display = "none";
+ }
+
+ async function loadActivityData(data = null) {
+ if (!section.isConnected) {
+ if (unsubscribeAboutRefresh) {
+ unsubscribeAboutRefresh();
+ unsubscribeAboutRefresh = null;
+ }
+ return;
+ }
+
+ try {
+ const stats = data || (await getWakaTimeData());
+
+ if (!stats || !stats.Weekly || !Array.isArray(stats.Weekly.Days)) {
+ hideActivity();
+ return;
+ }
+
+ const totalText = stats.Weekly.TotalText;
+ const dayBars = stats.Weekly.Days;
+ if (!totalText || dayBars.length === 0) {
+ hideActivity();
+ return;
+ }
+
+ const days = dayBars.map((bar) => ({
+ dayText: (bar.DayText || "").toLowerCase(),
+ timeText: bar.TimeText || "",
+ barWidthPercent:
+ bar.BarWidthPercent && typeof bar.BarWidthPercent === "string"
+ ? bar.BarWidthPercent.trim()
+ : "0%",
+ }));
+
+ renderWeeklyStats(statsContainer, totalText, days);
+ } catch {
+ hideActivity();
+ }
+ }
+
+ loadActivityData();
+
+ if (unsubscribeAboutRefresh) {
+ unsubscribeAboutRefresh();
+ }
+ unsubscribeAboutRefresh = subscribeWakaTimeRefresh((data) => {
+ void loadActivityData(data);
+ });
+}
+
+function renderWeeklyStats(statsContainer, totalText, days) {
+ statsContainer.innerHTML = "";
+
+ const width = 24;
+ const lines = [`total: ${totalText}`, ""];
+
+ days.forEach((day) => {
+ const label = (day.dayText || "").slice(0, 3).padEnd(3, " ");
+ const percent = clampPercent(day.barWidthPercent);
+ const bar = asciiBar(percent, width);
+ lines.push(`${label} |${bar}| ${day.timeText}`);
+ });
+
+ const pre = document.createElement("pre");
+ pre.className = "weekly-ascii";
+ pre.textContent = lines.join("\n");
+ statsContainer.appendChild(pre);
+}
+
+function clampPercent(value) {
+ const parsed = Number.parseFloat((value || "0").replace("%", ""));
+ if (!Number.isFinite(parsed)) return 0;
+ if (parsed < 0) return 0;
+ if (parsed > 100) return 100;
+ return parsed;
+}
+
+function asciiBar(percent, width) {
+ const filled = Math.round((percent / 100) * width);
+ return "#".repeat(filled) + "-".repeat(width - filled);
+}
diff --git a/internal/view/scripts/modules/blog.js b/internal/view/scripts/modules/blog.js
@@ -0,0 +1,183 @@
+import { isEditableTarget } from "./dom.js";
+import { clearAppTitleDetail, setAppTitleDetail, updateAppTitle } from "./header.js";
+
+const blogHandlers = {
+ keydown: null,
+ popstate: null,
+};
+
+export function initBlog() {
+ const section = document.getElementById("blog");
+ if (!section) return;
+
+ const listView = section.querySelector("#blog-list-view");
+ const postView = section.querySelector("#blog-post-view");
+ const backBtn = section.querySelector(".blog-back-btn");
+ const listItems = Array.from(section.querySelectorAll(".blog-list-item"));
+ if (!listView || !postView || listItems.length === 0) return;
+
+ let selectedIndex = 0;
+ const hasPrefilledPost = Boolean(document.getElementById("blog-post-content")?.children.length);
+ let postOpen = !postView.classList.contains("hidden") || window.location.pathname.startsWith("/blog/");
+ window.isBlogPostOpen = () => postOpen;
+
+ if (postOpen) {
+ listView.classList.add("hidden");
+ postView.classList.remove("hidden");
+ } else {
+ listView.classList.remove("hidden");
+ postView.classList.add("hidden");
+ }
+
+ function pathBlogID() {
+ return decodeURIComponent(window.location.pathname.match(/^\/blog\/([^/]+)$/)?.[1] || "");
+ }
+
+ function itemByBlogID(blogID) {
+ if (!blogID) return null;
+ return listItems.find((item) => item.dataset.id === blogID) || null;
+ }
+
+ function select(index) {
+ selectedIndex = index;
+ listItems.forEach((item, i) => item.classList.toggle("selected", i === index));
+ listItems[index]?.scrollIntoView({ behavior: "smooth", block: "nearest" });
+
+ const detail = listItems[index]?.querySelector(".blog-list-title")?.textContent?.trim() || "";
+ setAppTitleDetail("blog", detail);
+ updateAppTitle("blog");
+ }
+
+ function openPost(blogID, updateHistory = true) {
+ postOpen = true;
+ listView.classList.add("hidden");
+ postView.classList.remove("hidden");
+ postView.scrollTop = 0;
+
+ if (blogID && updateHistory) {
+ const path = `/blog/${encodeURIComponent(blogID)}`;
+ if (window.location.pathname !== path) {
+ history.replaceState({ tab: "blog" }, "", path);
+ }
+ }
+
+ updateAppTitle("blog");
+ }
+
+ function closePost(updateHistory = true) {
+ postOpen = false;
+ listView.classList.remove("hidden");
+ postView.classList.add("hidden");
+ clearAppTitleDetail("blog");
+
+ if (updateHistory && window.location.pathname !== "/blog") {
+ history.replaceState({ tab: "blog" }, "", "/blog");
+ }
+
+ updateAppTitle("blog");
+ }
+
+ listItems.forEach((item, index) => {
+ if (item.dataset.boundClick === "1") return;
+ item.dataset.boundClick = "1";
+
+ item.addEventListener("click", () => {
+ select(index);
+ openPost(item.dataset.id, true);
+ });
+ });
+
+ if (backBtn && backBtn.dataset.boundClick !== "1") {
+ backBtn.dataset.boundClick = "1";
+ backBtn.addEventListener("click", () => closePost(true));
+ }
+
+ if (blogHandlers.keydown) {
+ document.removeEventListener("keydown", blogHandlers.keydown);
+ }
+
+ blogHandlers.keydown = (e) => {
+ if (!section.isConnected) return;
+ if (window.isLightboxOpen?.()) return;
+ if (isEditableTarget(e.target)) return;
+
+ if (!postOpen) {
+ switch (e.key) {
+ case "ArrowUp":
+ e.preventDefault();
+ if (selectedIndex > 0) select(selectedIndex - 1);
+ break;
+ case "ArrowDown":
+ e.preventDefault();
+ if (selectedIndex < listItems.length - 1) select(selectedIndex + 1);
+ break;
+ case "ArrowRight":
+ case "Enter":
+ e.preventDefault();
+ listItems[selectedIndex]?.click();
+ break;
+ default:
+ break;
+ }
+ } else {
+ switch (e.key) {
+ case "ArrowLeft":
+ case "Escape":
+ e.preventDefault();
+ closePost(true);
+ break;
+ case "ArrowUp":
+ e.preventDefault();
+ postView.scrollBy({ top: -80, behavior: "smooth" });
+ break;
+ case "ArrowDown":
+ e.preventDefault();
+ postView.scrollBy({ top: 80, behavior: "smooth" });
+ break;
+ default:
+ break;
+ }
+ }
+ };
+
+ document.addEventListener("keydown", blogHandlers.keydown);
+
+ if (blogHandlers.popstate) {
+ window.removeEventListener("popstate", blogHandlers.popstate);
+ }
+
+ blogHandlers.popstate = () => {
+ if (!section.isConnected) return;
+
+ const currentID = pathBlogID();
+ if (!currentID) {
+ closePost(false);
+ return;
+ }
+
+ const item = itemByBlogID(currentID);
+ if (!item) return;
+
+ const idx = Number(item.dataset.index || 0);
+ select(idx);
+ openPost(currentID, false);
+
+ if (!document.getElementById("blog-post-content")?.children.length) {
+ item.click();
+ }
+ };
+
+ window.addEventListener("popstate", blogHandlers.popstate);
+
+ const pathID = pathBlogID();
+ const selectedFromPath = itemByBlogID(pathID);
+ const initialItem = selectedFromPath || section.querySelector(".blog-list-item.selected") || listItems[0];
+ if (!initialItem) return;
+
+ const initialIndex = Number(initialItem.dataset.index || 0);
+ select(initialIndex);
+
+ if (postOpen && !hasPrefilledPost) {
+ initialItem.click();
+ }
+}
diff --git a/internal/view/scripts/modules/blogDetail.js b/internal/view/scripts/modules/blogDetail.js
@@ -0,0 +1,60 @@
+import { setAppTitleDetail, updateAppTitle } from "./header.js";
+
+export function initBlogDetail() {
+ const heading = document.querySelector(".blog-article-title")?.textContent?.trim() || "";
+ if (heading) {
+ setAppTitleDetail("blog", heading);
+ updateAppTitle("blog");
+ }
+
+ const shareButtons = document.querySelectorAll(".blog-share-btn");
+ if (shareButtons.length === 0) return;
+
+ shareButtons.forEach((btn) => {
+ if (btn.dataset.boundClick === "1") return;
+ btn.dataset.boundClick = "1";
+
+ btn.addEventListener("click", async (e) => {
+ e.stopPropagation();
+ const button = e.currentTarget;
+ const blogId = button.dataset.blogId;
+ const title = button.dataset.blogTitle;
+ const url = `${window.location.origin}/blog/${blogId}`;
+
+ if (navigator.share) {
+ try {
+ await navigator.share({ title, url });
+ } catch {
+ const copied = await copyToClipboard(url);
+ if (copied) showCopiedState(button);
+ }
+ } else {
+ const copied = await copyToClipboard(url);
+ if (copied) showCopiedState(button);
+ }
+ });
+ });
+}
+
+function showCopiedState(button) {
+ const originalText = button.textContent;
+ button.textContent = "[Copied!]";
+ button.classList.add("copied");
+ window.setTimeout(() => {
+ button.textContent = originalText;
+ button.classList.remove("copied");
+ }, 2000);
+}
+
+async function copyToClipboard(text) {
+ if (!navigator.clipboard || typeof navigator.clipboard.writeText !== "function") {
+ return false;
+ }
+
+ try {
+ await navigator.clipboard.writeText(text);
+ return true;
+ } catch {
+ return false;
+ }
+}
diff --git a/internal/view/scripts/modules/dom.js b/internal/view/scripts/modules/dom.js
@@ -0,0 +1,8 @@
+export function isEditableTarget(target) {
+ return (
+ target instanceof HTMLInputElement ||
+ target instanceof HTMLTextAreaElement ||
+ target instanceof HTMLSelectElement ||
+ target?.isContentEditable
+ );
+}
diff --git a/internal/view/scripts/modules/experience.js b/internal/view/scripts/modules/experience.js
@@ -0,0 +1,121 @@
+import { isEditableTarget } from "./dom.js";
+
+const experienceHandlers = {
+ keydown: null,
+ popstate: null,
+};
+
+export function initExperience() {
+ const section = document.getElementById("experience");
+ if (!section) return;
+
+ const cards = Array.from(section.querySelectorAll(".exp-card"));
+ const timelines = Array.from(section.querySelectorAll(".exp-timeline"));
+ const rows = Array.from(section.querySelectorAll(".exp-row"));
+ if (cards.length === 0) return;
+
+ let selectedIndex = 0;
+
+ function experienceIDAt(index) {
+ return cards[index]?.dataset.id || rows[index]?.dataset.id || "";
+ }
+
+ function indexFromPath() {
+ const id = decodeURIComponent(window.location.pathname.match(/^\/experience\/([^/]+)$/)?.[1] || "");
+ if (!id) return -1;
+ return cards.findIndex((card) => card.dataset.id === id);
+ }
+
+ function updateExperienceURL(index) {
+ const id = experienceIDAt(index);
+ if (!id) return;
+
+ const path = `/experience/${encodeURIComponent(id)}`;
+ if (window.location.pathname !== path) {
+ history.replaceState({ tab: "experience" }, "", path);
+ }
+ }
+
+ function select(index, updateURL = true) {
+ selectedIndex = index;
+ cards.forEach((card, i) => card.classList.toggle("selected", i === index));
+ timelines.forEach((timeline, i) => timeline.classList.toggle("active", i === index));
+
+ if (updateURL) {
+ updateExperienceURL(index);
+ }
+ }
+
+ cards.forEach((card, i) => {
+ if (card.dataset.boundHover !== "1") {
+ card.dataset.boundHover = "1";
+ card.addEventListener("mouseenter", () => select(i));
+ }
+ });
+
+ timelines.forEach((timeline, i) => {
+ if (timeline.dataset.boundHover !== "1") {
+ timeline.dataset.boundHover = "1";
+ timeline.addEventListener("mouseenter", () => select(i));
+ }
+ });
+
+ if (experienceHandlers.keydown) {
+ document.removeEventListener("keydown", experienceHandlers.keydown);
+ }
+
+ experienceHandlers.keydown = (e) => {
+ if (!section.isConnected || window.isLightboxOpen?.()) return;
+ if (isEditableTarget(e.target)) return;
+
+ switch (e.key) {
+ case "ArrowUp":
+ e.preventDefault();
+ if (selectedIndex > 0) select(selectedIndex - 1);
+ break;
+ case "ArrowDown":
+ e.preventDefault();
+ if (selectedIndex < cards.length - 1) select(selectedIndex + 1);
+ break;
+ default:
+ break;
+ }
+ };
+
+ document.addEventListener("keydown", experienceHandlers.keydown);
+
+ if (experienceHandlers.popstate) {
+ window.removeEventListener("popstate", experienceHandlers.popstate);
+ }
+
+ experienceHandlers.popstate = () => {
+ if (!section.isConnected) return;
+ const index = indexFromPath();
+ if (index >= 0) {
+ select(index, false);
+ }
+ };
+
+ window.addEventListener("popstate", experienceHandlers.popstate);
+
+ section.querySelectorAll(".exp-image-hero, .exp-image-thumb").forEach((img) => {
+ if (img.dataset.boundClick === "1") return;
+ img.dataset.boundClick = "1";
+
+ img.addEventListener("click", () => {
+ const container = img.closest(".exp-card-images");
+ const allImages = container ? Array.from(container.querySelectorAll("img")).map((i) => i.src) : [];
+ const startIndex = allImages.indexOf(img.src);
+ window.openLightbox?.(allImages, startIndex);
+ });
+ });
+
+ const selectedCard = section.querySelector(".exp-card.selected") || cards[0];
+ const initialIndex = Math.max(cards.indexOf(selectedCard), 0);
+ const pathIndex = indexFromPath();
+ if (pathIndex >= 0) {
+ select(pathIndex, false);
+ } else {
+ select(initialIndex);
+ }
+}
diff --git a/internal/view/scripts/modules/header.js b/internal/view/scripts/modules/header.js
@@ -0,0 +1,212 @@
+import { isEditableTarget } from "./dom.js";
+
+const detailTitles = {};
+const headerHandlers = {
+ keydown: null,
+ afterSwap: null,
+ popstate: null,
+};
+
+export function initHeader() {
+ const headerEl = document.getElementById("tabs");
+ if (!headerEl) return;
+
+ const order = (headerEl.getAttribute("data-tabs") || "about").split(",").filter(Boolean);
+ const fallback = order[0] || "about";
+
+ function goTo(tab) {
+ const link = document.querySelector(`.tab[data-tab="${tab}"]`);
+ if (!link) return;
+
+ const path = link.getAttribute("hx-get") || link.getAttribute("href");
+ if (window.htmx && path) {
+ setActiveTabUI(tab);
+ if (window.location.pathname !== path) {
+ history.pushState({ tab }, "", path);
+ }
+
+ window.htmx.ajax("GET", path, {
+ target: "#content",
+ swap: "innerHTML",
+ });
+ return;
+ }
+
+ link.click();
+ }
+
+ if (headerHandlers.keydown) {
+ document.removeEventListener("keydown", headerHandlers.keydown);
+ }
+
+ headerHandlers.keydown = (e) => {
+ if (isEditableTarget(e.target)) return;
+ if (typeof window.isLightboxOpen === "function" && window.isLightboxOpen()) return;
+
+ const isBlogPostOpen = typeof window.isBlogPostOpen === "function" && window.isBlogPostOpen();
+
+ const current = currentFromPath(order, fallback);
+ const index = order.indexOf(current);
+
+ if (e.key >= "1" && e.key <= String(order.length)) {
+ const i = Number(e.key) - 1;
+ if (order[i]) {
+ e.preventDefault();
+ if (order[i] === current) return;
+ goTo(order[i]);
+ }
+ return;
+ }
+
+ if (isBlogPostOpen) return;
+
+ switch (e.key) {
+ case "Tab": {
+ e.preventDefault();
+ const next = order[(index + (e.shiftKey ? -1 : 1) + order.length) % order.length];
+ goTo(next);
+ break;
+ }
+ case "ArrowLeft":
+ if (index > 0) {
+ e.preventDefault();
+ goTo(order[index - 1]);
+ }
+ break;
+ case "ArrowRight":
+ if (index < order.length - 1) {
+ e.preventDefault();
+ goTo(order[index + 1]);
+ }
+ break;
+ default:
+ break;
+ }
+ };
+
+ document.addEventListener("keydown", headerHandlers.keydown);
+
+ if (headerHandlers.afterSwap) {
+ document.removeEventListener("htmx:afterSwap", headerHandlers.afterSwap);
+ }
+
+ headerHandlers.afterSwap = (e) => {
+ if (!e.target) return;
+
+ switch (e.target.id) {
+ case "content":
+ setActiveTabUI(currentFromPath(order, fallback));
+ break;
+ case "project-detail":
+ case "blog-post-content":
+ updateAppTitle();
+ break;
+ default:
+ break;
+ }
+ };
+
+ document.addEventListener("htmx:afterSwap", headerHandlers.afterSwap);
+
+ if (headerHandlers.popstate) {
+ window.removeEventListener("popstate", headerHandlers.popstate);
+ }
+
+ headerHandlers.popstate = () => {
+ setActiveTabUI(currentFromPath(order, fallback));
+ };
+
+ window.addEventListener("popstate", headerHandlers.popstate);
+
+ setActiveTabUI(currentFromPath(order, fallback));
+}
+
+export function updateAppTitle(activeTab = "") {
+ const headerEl = document.getElementById("tabs");
+ if (!headerEl) return;
+
+ const order = (headerEl.getAttribute("data-tabs") || "about").split(",").filter(Boolean);
+ const fallback = order[0] || "about";
+ const tab = activeTab || currentFromPath(order, fallback);
+
+ const tabEl = document.querySelector(`.tab[data-tab="${tab}"]`);
+ const tabLabel = tabEl?.getAttribute("data-label") || tab;
+ const siteName = document.getElementById("app")?.dataset.bootTitle || "prime8.dev";
+
+ let detailLabel = "";
+ const path = window.location.pathname;
+
+ switch (tab) {
+ case "projects":
+ if (/^\/projects\/[^/]+$/.test(path)) {
+ const projectID = path.match(/^\/projects\/([^/]+)$/)?.[1] || "";
+ detailLabel =
+ detailTitles.projects ||
+ document.querySelector("#project-detail h2")?.textContent?.trim() ||
+ selectedListTitle(".list-item[data-id]", ".list-title", projectID);
+ }
+ break;
+ case "blog":
+ if (/^\/blog\/[^/]+$/.test(path)) {
+ const blogID = path.match(/^\/blog\/([^/]+)$/)?.[1] || "";
+ detailLabel =
+ detailTitles.blog ||
+ document.querySelector(".blog-article-title")?.textContent?.trim() ||
+ selectedListTitle(".blog-list-item[data-id]", ".blog-list-title", blogID);
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (detailLabel) {
+ document.title = `${detailLabel} | ${tabLabel} | ${siteName}`;
+ return;
+ }
+
+ document.title = `${tabLabel} | ${siteName}`;
+}
+
+export function setAppTitleDetail(tab, value) {
+ if (value) {
+ detailTitles[tab] = value;
+ } else {
+ delete detailTitles[tab];
+ }
+}
+
+export function clearAppTitleDetail(tab) {
+ delete detailTitles[tab];
+}
+
+function setActiveTabUI(tab) {
+ document.querySelectorAll(".tab").forEach((el) => {
+ const isActive = el.getAttribute("data-tab") === tab;
+ el.classList.toggle("active", isActive);
+ if (isActive) {
+ el.setAttribute("aria-current", "page");
+ } else {
+ el.removeAttribute("aria-current");
+ }
+ });
+
+ updateAppTitle(tab);
+}
+
+function currentFromPath(order, fallback) {
+ const path = window.location.pathname;
+ if (path === "/" || path === "") return fallback;
+ const tab = path.replace(/^\//, "").split("/")[0];
+ return order.includes(tab) ? tab : fallback;
+}
+
+function selectedListTitle(selector, titleSelector, id) {
+ if (!id) return "";
+
+ const normalized = decodeURIComponent(id);
+ const items = Array.from(document.querySelectorAll(selector));
+ const item = items.find((el) => (el.dataset.id || "") === normalized);
+ if (!item) return "";
+
+ return item.querySelector(titleSelector)?.textContent?.trim() || "";
+}
diff --git a/internal/view/scripts/modules/help.js b/internal/view/scripts/modules/help.js
@@ -0,0 +1,80 @@
+import { isEditableTarget } from "./dom.js";
+
+const SHORTCUTS = [
+ { key: "◀ ▶", description: "navigate tabs" },
+ { key: "▲ ▼", description: "scroll content" },
+ { key: "tab", description: "cycle tabs" },
+ { key: "1-4", description: "switch tabs" },
+ { key: "enter", description: "open item" },
+ { key: "?", description: "toggle help" },
+];
+
+const helpHandlers = {
+ keydown: null,
+};
+
+export function initHelp() {
+ const overlay = createHelpOverlay();
+
+ if (helpHandlers.keydown) {
+ document.removeEventListener("keydown", helpHandlers.keydown);
+ }
+
+ helpHandlers.keydown = (e) => {
+ if (isEditableTarget(e.target)) return;
+
+ switch (e.key) {
+ case "?":
+ e.preventDefault();
+ overlay.classList.toggle("visible");
+ break;
+ case "Escape":
+ if (overlay.classList.contains("visible")) {
+ e.preventDefault();
+ overlay.classList.remove("visible");
+ }
+ break;
+ default:
+ break;
+ }
+ };
+
+ document.addEventListener("keydown", helpHandlers.keydown);
+}
+
+function createHelpOverlay() {
+ const existing = document.getElementById("help-overlay");
+ if (existing) return existing;
+
+ const overlay = document.createElement("div");
+ overlay.id = "help-overlay";
+ overlay.setAttribute("aria-hidden", "true");
+
+ overlay.innerHTML = `
+ <div class="help-modal" role="dialog" aria-modal="true" aria-label="keyboard shortcuts">
+ <div class="help-header">
+ <span class="help-title">help</span>
+ <span class="help-close">press ? to close</span>
+ </div>
+ <div class="help-content">
+ ${SHORTCUTS.map(
+ (shortcut) => `
+ <div class="help-row">
+ <span class="help-key">${shortcut.key}</span>
+ <span class="help-desc">${shortcut.description}</span>
+ </div>
+ `,
+ ).join("")}
+ </div>
+ </div>
+ `;
+
+ overlay.addEventListener("click", (e) => {
+ if (e.target === overlay) {
+ overlay.classList.remove("visible");
+ }
+ });
+
+ document.body.appendChild(overlay);
+ return overlay;
+}
diff --git a/internal/view/scripts/modules/layout.js b/internal/view/scripts/modules/layout.js
@@ -0,0 +1,144 @@
+import {
+ getWakaTimeData,
+ startWakaTimeRefreshTimer,
+ subscribeWakaTimeRefresh,
+} from "./wakatime.js";
+
+const BOOT_CHARS = "▓▒░█▄▀■□▪▫●○◆◇";
+const SCRAMBLE_DURATION = 400;
+const SCRAMBLE_INTERVAL = 30;
+const HOLD_DURATION = 450;
+const FADE_DURATION = 300;
+
+let bootPlayed = false;
+let statusTimer = 0;
+let unsubscribeWakaFooter = null;
+
+export async function initLayout() {
+ await playBootAnimation();
+
+ startStatusClock();
+
+ await loadWakaFooter();
+ if (unsubscribeWakaFooter) {
+ unsubscribeWakaFooter();
+ }
+ unsubscribeWakaFooter = subscribeWakaTimeRefresh((data) => {
+ void loadWakaFooter(data);
+ });
+ startWakaTimeRefreshTimer();
+}
+
+function sleep(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function scrambleText(length) {
+ return Array.from(
+ { length },
+ () => BOOT_CHARS[Math.floor(Math.random() * BOOT_CHARS.length)],
+ ).join("");
+}
+
+async function scrambleReveal(element, target, duration) {
+ const steps = Math.floor(duration / SCRAMBLE_INTERVAL);
+ const length = target.length;
+
+ for (let i = 0; i <= steps; i++) {
+ const progress = i / steps;
+ const revealedCount = Math.floor(progress * length);
+ const revealed = target.slice(0, revealedCount);
+ const scrambled = scrambleText(length - revealedCount);
+ element.textContent = revealed + scrambled;
+ await sleep(SCRAMBLE_INTERVAL);
+ }
+
+ element.textContent = target;
+}
+
+function createBootOverlay(title, subtitle) {
+ const overlay = document.createElement("div");
+ overlay.id = "boot-overlay";
+ overlay.innerHTML =
+ '<div class="boot-terminal"><div class="boot-text"><span class="boot-title"></span><span class="boot-subtitle"></span></div></div>';
+
+ const bootTitle = overlay.querySelector(".boot-title");
+ const bootSubtitle = overlay.querySelector(".boot-subtitle");
+ if (bootTitle) bootTitle.textContent = scrambleText(title.length);
+ if (bootSubtitle) bootSubtitle.textContent = scrambleText(subtitle.length);
+
+ document.body.appendChild(overlay);
+ return overlay;
+}
+
+async function playBootAnimation() {
+ const app = document.getElementById("app");
+ if (!app) return;
+
+ if (bootPlayed || app.classList.contains("ready")) {
+ bootPlayed = true;
+ app.classList.add("ready");
+ return;
+ }
+
+ const title = app.dataset.bootTitle || "prime8.dev";
+ const subtitle = app.dataset.bootSubtitle || "www.prime8.dev";
+
+ const overlay = createBootOverlay(title, subtitle);
+ const bootTitle = overlay.querySelector(".boot-title");
+ const bootSubtitle = overlay.querySelector(".boot-subtitle");
+
+ await sleep(100);
+ if (bootTitle) await scrambleReveal(bootTitle, title, SCRAMBLE_DURATION);
+ if (bootSubtitle)
+ await scrambleReveal(bootSubtitle, subtitle, SCRAMBLE_DURATION);
+
+ await sleep(HOLD_DURATION);
+ overlay.classList.add("fade-out");
+ await sleep(FADE_DURATION);
+
+ overlay.remove();
+ app.classList.add("ready", "boot-reveal");
+ bootPlayed = true;
+}
+
+function updateStatusTime() {
+ const el = document.getElementById("status-time");
+ if (!el) return;
+
+ const now = new Date();
+ const time = now.toLocaleTimeString();
+ const date = now.toLocaleDateString();
+ el.textContent = `${date} ${time}`;
+}
+
+function startStatusClock() {
+ if (statusTimer) {
+ clearTimeout(statusTimer);
+ }
+
+ const tick = () => {
+ updateStatusTime();
+ const delay = 1000 - (Date.now() % 1000) || 1000;
+ statusTimer = window.setTimeout(tick, delay);
+ };
+
+ tick();
+}
+
+async function loadWakaFooter(data = null) {
+ const link = document.getElementById("wakatime-link");
+ if (!link) return;
+
+ try {
+ const stats = data || (await getWakaTimeData());
+ if (!stats || !stats.AllTime || !stats.AllTime.TotalText) {
+ link.style.display = "none";
+ return;
+ }
+
+ link.textContent = stats.AllTime.TotalText;
+ } catch {
+ link.style.display = "none";
+ }
+}
diff --git a/internal/view/scripts/modules/lightbox.js b/internal/view/scripts/modules/lightbox.js
@@ -0,0 +1,87 @@
+let lightboxInit = false;
+const lightboxHandlers = {
+ keydown: null,
+};
+
+export function initLightbox() {
+ if (lightboxInit) return;
+
+ const lightbox = document.getElementById("image-lightbox");
+ const lightboxImage = lightbox?.querySelector(".lightbox-image");
+ const lightboxOverlay = lightbox?.querySelector(".lightbox-overlay");
+ const lightboxClose = lightbox?.querySelector(".lightbox-close");
+ if (!lightbox || !lightboxImage) return;
+
+ let lightboxImages = [];
+ let currentLightboxIndex = 0;
+
+ window.isLightboxOpen = () => !lightbox.classList.contains("hidden");
+ window.openLightbox = (images, startIndex = 0) => {
+ if (!Array.isArray(images) || images.length === 0) return;
+
+ lightboxImages = images;
+ currentLightboxIndex = startIndex;
+ lightboxImage.src = images[startIndex];
+ lightbox.classList.remove("hidden");
+ };
+
+ function closeLightbox() {
+ lightbox.classList.add("hidden");
+ lightboxImages = [];
+ currentLightboxIndex = 0;
+ }
+
+ function navigateLightboxImage(direction) {
+ if (lightboxImages.length <= 1) return;
+
+ let delta = 0;
+ switch (direction) {
+ case "left":
+ delta = -1;
+ break;
+ case "right":
+ delta = 1;
+ break;
+ default:
+ return;
+ }
+
+ currentLightboxIndex =
+ (currentLightboxIndex + delta + lightboxImages.length) % lightboxImages.length;
+ lightboxImage.src = lightboxImages[currentLightboxIndex];
+ }
+
+ window.navigateLightboxImage = navigateLightboxImage;
+
+ lightboxOverlay?.addEventListener("click", closeLightbox);
+ lightboxClose?.addEventListener("click", closeLightbox);
+
+ if (lightboxHandlers.keydown) {
+ document.removeEventListener("keydown", lightboxHandlers.keydown);
+ }
+
+ lightboxHandlers.keydown = (e) => {
+ if (lightbox.classList.contains("hidden")) return;
+
+ switch (e.key) {
+ case "Escape":
+ closeLightbox();
+ break;
+ case "ArrowLeft":
+ e.preventDefault();
+ e.stopPropagation();
+ navigateLightboxImage("left");
+ break;
+ case "ArrowRight":
+ e.preventDefault();
+ e.stopPropagation();
+ navigateLightboxImage("right");
+ break;
+ default:
+ break;
+ }
+ };
+
+ document.addEventListener("keydown", lightboxHandlers.keydown);
+ lightboxInit = true;
+}
diff --git a/internal/view/scripts/modules/projects.js b/internal/view/scripts/modules/projects.js
@@ -0,0 +1,138 @@
+import { isEditableTarget } from "./dom.js";
+import { setAppTitleDetail, updateAppTitle } from "./header.js";
+
+const projectsHandlers = {
+ keydown: null,
+};
+
+export function initProjects() {
+ const section = document.getElementById("projects");
+ if (!section) return;
+
+ const items = Array.from(section.querySelectorAll(".list-item"));
+ if (items.length === 0) return;
+
+ const detailView = section.querySelector(".detail-view");
+
+ let selectedIndex = 0;
+
+ function readmeScroller() {
+ if (!detailView) return null;
+
+ const readme = detailView.querySelector(".readme-content");
+ if (readme && readme.scrollHeight > readme.clientHeight) {
+ return readme;
+ }
+
+ if (detailView.scrollHeight > detailView.clientHeight) {
+ return detailView;
+ }
+
+ return null;
+ }
+
+ function setSelected(selectedItem) {
+ items.forEach((item) => item.classList.toggle("selected", item === selectedItem));
+ selectedIndex = items.indexOf(selectedItem);
+ if (selectedIndex < 0) selectedIndex = 0;
+
+ selectedItem.scrollIntoView({ behavior: "smooth", block: "nearest" });
+
+ const detail = selectedItem.querySelector(".list-title")?.textContent?.trim() || "";
+ setAppTitleDetail("projects", detail);
+ updateAppTitle("projects");
+ }
+
+ function updateProjectURL(projectID) {
+ if (!projectID) return;
+ const path = `/projects/${encodeURIComponent(projectID)}`;
+ if (window.location.pathname !== path) {
+ history.replaceState({ tab: "projects" }, "", path);
+ updateAppTitle("projects");
+ }
+ }
+
+ function loadDetailForItem(item) {
+ const url = item.getAttribute("hx-get");
+ if (!url) return;
+
+ updateProjectURL(item.dataset.id);
+
+ if (window.htmx && typeof window.htmx.ajax === "function") {
+ window.htmx.ajax("GET", url, "#project-detail");
+ return;
+ }
+
+ item.click();
+ }
+
+ function selectByIndex(index, loadDetail) {
+ if (index < 0 || index >= items.length) return;
+ setSelected(items[index]);
+ if (loadDetail) loadDetailForItem(items[index]);
+ }
+
+ function openExternalForIndex(index) {
+ if (index < 0 || index >= items.length) return;
+ const url = items[index]?.dataset.url || "";
+ if (!url) return;
+ window.open(url, "_blank", "noopener,noreferrer");
+ }
+
+ items.forEach((item, index) => {
+ if (item.dataset.boundClick === "1") return;
+ item.dataset.boundClick = "1";
+ item.addEventListener("click", () => {
+ selectedIndex = index;
+ setSelected(item);
+ updateProjectURL(item.dataset.id);
+ });
+ });
+
+ if (projectsHandlers.keydown) {
+ document.removeEventListener("keydown", projectsHandlers.keydown);
+ }
+
+ projectsHandlers.keydown = (e) => {
+ if (!section.isConnected) return;
+ if (isEditableTarget(e.target)) return;
+ if (window.isLightboxOpen?.()) return;
+
+ if ((e.key === "ArrowUp" || e.key === "ArrowDown") && detailView?.matches(":hover")) {
+ const scroller = readmeScroller();
+ if (scroller) {
+ e.preventDefault();
+ scroller.scrollBy({ top: e.key === "ArrowUp" ? -80 : 80, behavior: "smooth" });
+ return;
+ }
+ }
+
+ switch (e.key) {
+ case "ArrowUp":
+ e.preventDefault();
+ selectByIndex(selectedIndex - 1, true);
+ break;
+ case "ArrowDown":
+ e.preventDefault();
+ selectByIndex(selectedIndex + 1, true);
+ break;
+ case "Enter":
+ e.preventDefault();
+ openExternalForIndex(selectedIndex);
+ break;
+ default:
+ break;
+ }
+ };
+
+ document.addEventListener("keydown", projectsHandlers.keydown);
+
+ const initialItem = section.querySelector(".list-item.selected") || items[0];
+ setSelected(initialItem);
+
+ const detailContainer = document.getElementById("project-detail");
+ const needsInitialFetch = detailContainer?.querySelector("[data-project-placeholder='true']");
+ if (needsInitialFetch) {
+ setTimeout(() => loadDetailForItem(initialItem), 0);
+ }
+}
diff --git a/internal/view/scripts/modules/wakatime.js b/internal/view/scripts/modules/wakatime.js
@@ -0,0 +1,81 @@
+const WAKATIME_TTL_MS = 5 * 60 * 1000;
+
+const wakatimeCache = {
+ data: null,
+ timestamp: 0,
+ pending: null,
+};
+const wakatimeListeners = new Set();
+let wakatimeRefreshTimer = 0;
+let wakatimeRefreshing = null;
+
+export async function getWakaTimeData(force = false) {
+ if (!force && wakatimeCache.data && isFresh(wakatimeCache.timestamp)) {
+ return wakatimeCache.data;
+ }
+
+ if (wakatimeCache.pending) {
+ return wakatimeCache.pending;
+ }
+
+ wakatimeCache.pending = fetch("/api/wakatime", { credentials: "same-origin" })
+ .then((res) => (res.ok ? res.json() : null))
+ .then((data) => {
+ if (data) {
+ wakatimeCache.data = data;
+ wakatimeCache.timestamp = Date.now();
+ }
+ return data;
+ })
+ .catch(() => null)
+ .finally(() => {
+ wakatimeCache.pending = null;
+ });
+
+ return wakatimeCache.pending;
+}
+
+export function subscribeWakaTimeRefresh(listener) {
+ if (typeof listener !== "function") {
+ return () => {};
+ }
+
+ wakatimeListeners.add(listener);
+ return () => {
+ wakatimeListeners.delete(listener);
+ };
+}
+
+export function startWakaTimeRefreshTimer() {
+ if (wakatimeRefreshTimer) return;
+
+ wakatimeRefreshTimer = window.setInterval(() => {
+ void refreshWakaTimeData();
+ }, WAKATIME_TTL_MS);
+}
+
+function isFresh(timestamp) {
+ return Date.now() - timestamp < WAKATIME_TTL_MS;
+}
+
+async function refreshWakaTimeData() {
+ if (wakatimeRefreshing) {
+ return wakatimeRefreshing;
+ }
+
+ wakatimeRefreshing = getWakaTimeData(true)
+ .then((data) => {
+ for (const listener of wakatimeListeners) {
+ try {
+ listener(data);
+ } catch {}
+ }
+
+ return data;
+ })
+ .finally(() => {
+ wakatimeRefreshing = null;
+ });
+
+ return wakatimeRefreshing;
+}
diff --git a/internal/view/styles/about.css b/internal/view/styles/about.css
@@ -0,0 +1,276 @@
+.about-layout {
+ display: flex;
+ width: 100%;
+ height: 100%;
+}
+
+.about-left {
+ width: 320px;
+ flex-shrink: 0;
+ border-right: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ padding: 16px;
+ gap: 16px;
+ overflow-y: auto;
+}
+
+.about-right {
+ flex: 1;
+ padding: 24px 32px;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ gap: 14px;
+}
+
+.profile-image {
+ width: 100%;
+ max-width: 280px;
+ height: auto;
+ border-radius: 4px;
+ border: 1px solid var(--border);
+}
+
+.contact-box {
+ border: 1px solid var(--border);
+ padding: 12px;
+ max-width: 280px;
+}
+
+.box-title {
+ color: var(--accent);
+ font-size: var(--fs-12);
+ margin-bottom: 4px;
+ text-align: left;
+}
+
+.shell-prompt {
+ color: var(--fg-dim);
+ font-size: var(--fs-14);
+ margin-bottom: 3px;
+}
+
+.shell-user {
+ color: var(--green);
+}
+
+.shell-dollar {
+ color: var(--green);
+}
+
+.shell-host {
+ color: var(--accent);
+}
+
+.shell-path {
+ color: var(--cyan);
+}
+
+.shell-cmd {
+ color: var(--fg);
+}
+
+.name-ascii {
+ margin: 0;
+ color: var(--fg);
+ font-size: clamp(6px, 0.7vw + 4px, 12px);
+ line-height: 1;
+ white-space: pre;
+ overflow: clip;
+}
+
+@media (max-width: 1024px) {
+ .name-ascii {
+ font-size: clamp(6px, 0.5vw + 4px, var(--fs-9));
+ line-height: 1;
+ }
+}
+
+.contact-item {
+ display: flex;
+ margin-bottom: 4px;
+ font-size: var(--fs-13);
+ min-width: 0;
+}
+
+.contact-item .label {
+ color: var(--fg-dim);
+ width: 70px;
+ flex-shrink: 0;
+}
+
+.contact-item a {
+ color: var(--light-blue);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ direction: rtl;
+ text-align: left;
+}
+
+.contact-item a:hover {
+ opacity: 0.8;
+}
+
+.section {
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.section .dim {
+ color: var(--fg-dim);
+ font-size: inherit;
+}
+
+.section .bio {
+ color: var(--fg);
+ font-size: var(--fs-14);
+ font-style: normal;
+ line-height: 1.6;
+ margin: 8px 0 0;
+}
+
+.info-columns {
+ display: flex;
+ gap: 30px;
+}
+
+.info-column {
+ flex: 1;
+}
+
+.info-row {
+ display: flex;
+ font-size: var(--fs-14);
+ margin-bottom: 2px;
+}
+
+.info-row .label {
+ color: var(--fg-dim);
+ width: 100px;
+ flex-shrink: 0;
+}
+
+.tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.weekly-stats {
+ background: var(--bg-alt);
+ border: 1px solid var(--border);
+ padding: 12px;
+}
+
+.weekly-ascii {
+ margin: 0;
+ color: var(--fg);
+ font-size: var(--fs-12);
+ line-height: 1.4;
+ white-space: pre;
+ overflow-x: auto;
+}
+
+.weekly-stats .loading-text {
+ color: var(--fg-dim);
+ font-style: italic;
+ text-align: center;
+ padding: 20px 0;
+}
+
+.weekly-total {
+ font-size: var(--fs-16);
+ font-weight: 500;
+ color: var(--accent);
+ margin-bottom: 12px;
+}
+
+.weekly-days {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.weekly-day-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: var(--fs-12);
+}
+
+.weekly-day-name {
+ width: 32px;
+ flex-shrink: 0;
+ color: var(--fg-dim);
+}
+
+.weekly-day-bar-container {
+ flex: 1;
+ height: 8px;
+ background: var(--bg);
+ border-radius: 2px;
+ overflow: hidden;
+}
+
+.weekly-day-bar {
+ height: 100%;
+ background: var(--accent);
+ border-radius: 2px;
+}
+
+.weekly-day-time {
+ min-width: 50px;
+ flex-shrink: 0;
+ text-align: right;
+ color: var(--fg-dim);
+ white-space: nowrap;
+}
+
+@media (max-width: 768px) {
+ .about-layout {
+ flex-direction: column;
+ }
+
+ .about-left {
+ flex-direction: row;
+ width: 100%;
+ border-right: none;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .profile-image {
+ width: 120px;
+ height: 120px;
+ object-fit: cover;
+ }
+
+ .contact-box {
+ flex: 1;
+ max-width: none;
+ min-width: 0;
+ }
+
+ .about-right {
+ padding: 16px;
+ display: block;
+ }
+
+ .section {
+ margin-bottom: 16px;
+ }
+
+ .name-ascii {
+ font-size: clamp(5px, 0.45vw + 3.2px, var(--fs-8));
+ line-height: 1;
+ }
+
+ .info-columns {
+ flex-direction: column;
+ gap: 12px;
+ }
+}
diff --git a/internal/view/styles/blog.css b/internal/view/styles/blog.css
@@ -0,0 +1,197 @@
+#blog {
+ width: 100%;
+}
+
+.blog-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+ gap: 8px;
+}
+
+.blog-list-view {
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+ overflow-y: auto;
+ padding: 24px 28px;
+}
+
+.blog-list-view.hidden {
+ display: none;
+}
+
+.blog-fs-shell {
+ width: 100%;
+ max-width: 860px;
+ border: 1px solid var(--border);
+ background: var(--bg-alt);
+}
+
+.blog-fs-header {
+ color: var(--accent);
+ border-bottom: 1px solid var(--border);
+ padding: 10px 14px;
+ font-size: var(--fs-14);
+}
+
+.blog-list-columns {
+ display: grid;
+ grid-template-columns: 92px 112px minmax(220px, 1.2fr) minmax(0, 0.8fr) 24px;
+ gap: 10px;
+ color: var(--fg-dim);
+ border-bottom: 1px solid var(--border);
+ padding: 8px 14px;
+ font-size: var(--fs-12);
+}
+
+.blog-list-container {
+ width: 100%;
+ padding: 8px;
+}
+
+.blog-list-item {
+ display: grid;
+ grid-template-columns: 92px 112px minmax(220px, 1.2fr) minmax(0, 0.8fr) 24px;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 6px;
+ border: 1px solid var(--border);
+ margin-bottom: 8px;
+ cursor: pointer;
+ transition: background 0.15s ease, border-color 0.15s ease;
+}
+
+.blog-list-item:hover {
+ background: var(--selection);
+ border-color: var(--accent);
+}
+
+.blog-list-item.selected {
+ background: var(--bg-alt);
+ border-color: var(--accent);
+}
+
+.blog-list-date {
+ color: var(--fg-dim);
+ font-size: var(--fs-13);
+}
+
+.blog-list-mode {
+ color: var(--blue);
+ font-size: var(--fs-12);
+}
+
+.blog-list-file {
+ color: var(--light-blue);
+ font-weight: 500;
+ font-size: var(--fs-14);
+ text-decoration: underline;
+ text-decoration-color: transparent;
+ transition: text-decoration-color 0.15s;
+}
+
+.blog-list-name {
+ min-width: 0;
+}
+
+.blog-list-title {
+ display: none;
+}
+
+.blog-list-desc {
+ color: var(--fg-dim);
+ font-size: var(--fs-12);
+ max-width: 34ch;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.blog-list-arrow {
+ color: var(--fg-dim);
+ font-size: var(--fs-13);
+ justify-self: end;
+}
+
+.blog-list-item:hover .blog-list-file,
+.blog-list-item.selected .blog-list-file {
+ text-decoration-color: currentColor;
+}
+
+.blog-post-view {
+ display: flex;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+ overflow-y: auto;
+ padding: 32px;
+}
+
+.blog-post-view.hidden {
+ display: none;
+}
+
+.blog-post-container {
+ max-width: 760px;
+ width: 100%;
+}
+
+.blog-back-btn {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--fg-dim);
+ padding: 6px 12px;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: var(--fs-13);
+ margin-bottom: 14px;
+}
+
+.blog-back-btn:hover {
+ background: var(--bg-alt);
+ color: var(--accent);
+ border-color: var(--accent);
+}
+
+@media (max-width: 768px) {
+ .blog-list-view {
+ padding: 12px;
+ }
+
+ .blog-fs-header {
+ font-size: var(--fs-12);
+ }
+
+ .blog-list-columns,
+ .blog-list-item {
+ grid-template-columns: 98px minmax(0, 1fr) 18px;
+ }
+
+ .blog-list-columns .col-mode,
+ .blog-list-columns .col-desc,
+ .blog-list-mode {
+ display: none;
+ }
+
+ .blog-list-date {
+ font-size: var(--fs-11);
+ }
+
+ .blog-list-desc {
+ display: none;
+ }
+
+ .blog-list-container {
+ padding: 6px;
+ }
+
+ .blog-post-view {
+ padding: 16px;
+ }
+}
diff --git a/internal/view/styles/blog_detail.css b/internal/view/styles/blog_detail.css
@@ -0,0 +1,149 @@
+.blog-article-header {
+ margin-bottom: 18px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid var(--border);
+}
+
+.blog-article-header-top {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+}
+
+.blog-article-title {
+ font-size: var(--fs-24);
+ font-weight: 500;
+ color: var(--accent);
+ margin: 0 0 8px;
+ line-height: 1.3;
+ overflow-wrap: anywhere;
+}
+
+.blog-share-btn {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--fg-dim);
+ padding: 6px 12px;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: var(--fs-12);
+}
+
+.blog-share-btn:hover {
+ background: var(--bg-alt);
+ color: var(--accent);
+ border-color: var(--accent);
+}
+
+.blog-share-btn.copied {
+ color: var(--green);
+ border-color: var(--green);
+}
+
+.blog-article-meta {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.blog-article-date {
+ color: var(--fg-dim);
+ font-size: var(--fs-13);
+}
+
+.blog-share-btn-mobile {
+ display: none;
+}
+
+.blog-article-content {
+ font-size: var(--fs-14);
+ line-height: 1.7;
+ color: var(--fg);
+ padding-bottom: 32px;
+}
+
+.blog-article-content p {
+ margin-bottom: 16px;
+}
+
+.blog-article-content h2 {
+ font-size: var(--fs-20);
+ color: var(--accent);
+ margin: 32px 0 12px;
+}
+
+.blog-article-content h3 {
+ font-size: var(--fs-17);
+ color: var(--accent);
+ margin: 24px 0 10px;
+}
+
+.blog-article-content pre {
+ display: block;
+ background: var(--bg-alt);
+ border: 1px solid var(--border);
+ padding: 16px;
+ margin: 16px 0;
+ overflow-x: auto;
+ font-size: var(--fs-13);
+}
+
+.blog-article-content code {
+ background: var(--bg-alt);
+ padding: 2px 6px;
+ font-size: var(--fs-13);
+}
+
+.blog-article-content pre code {
+ background: transparent;
+ padding: 0;
+ font-size: inherit;
+}
+
+.blog-article-content a {
+ color: var(--light-blue);
+}
+
+.blog-article-content ul,
+.blog-article-content ol {
+ margin: 12px 0;
+ padding-left: 24px;
+}
+
+.blog-article-content table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 16px 0;
+ font-size: var(--fs-13);
+}
+
+.blog-article-content th,
+.blog-article-content td {
+ border: 1px solid var(--border);
+ padding: 10px 14px;
+ text-align: left;
+}
+
+.blog-article-content th {
+ background: var(--bg-alt);
+ color: var(--accent);
+}
+
+@media (max-width: 768px) {
+ .blog-article-title {
+ font-size: var(--fs-20);
+ }
+
+ .blog-share-btn-desktop {
+ display: none;
+ }
+
+ .blog-share-btn-mobile {
+ display: block;
+ }
+
+ .blog-article-content {
+ font-size: var(--fs-13);
+ }
+}
diff --git a/internal/view/styles/experience.css b/internal/view/styles/experience.css
@@ -0,0 +1,326 @@
+#experience {
+ width: 100%;
+}
+
+.experience-layout {
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+ width: 100%;
+ padding: 24px 32px;
+ gap: 24px;
+ justify-content: center;
+}
+
+.experience-detail {
+ flex: 0 1 980px;
+ width: 100%;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 4px 20px 4px 4px;
+}
+
+.exp-row {
+ display: flex;
+}
+
+.exp-card {
+ background: var(--bg-alt);
+ border: 2px solid var(--border);
+ border-radius: 4px;
+ padding: 24px;
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.exp-card.selected {
+ border-color: var(--accent);
+}
+
+.exp-timeline {
+ width: 64px;
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding-top: 24px;
+ position: relative;
+}
+
+.timeline-dot {
+ width: 12px;
+ height: 12px;
+ border: 2px solid var(--fg-dim);
+ border-radius: 50%;
+ background: transparent;
+}
+
+.timeline-dot-end {
+ position: absolute;
+ bottom: 8px;
+}
+
+.exp-timeline.has-end-dot .timeline-line {
+ bottom: 14px;
+}
+
+.exp-timeline.active .timeline-dot-end {
+ border-color: var(--accent);
+ background: var(--accent);
+}
+
+.exp-timeline.active .timeline-dot {
+ border-color: var(--accent);
+ background: var(--accent);
+}
+
+.timeline-year {
+ font-size: var(--fs-12);
+ color: var(--fg-dim);
+ margin-top: 4px;
+}
+
+.timeline-line {
+ position: absolute;
+ top: 56px;
+ bottom: -16px;
+ width: 2px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: repeating-linear-gradient(
+ to bottom,
+ var(--border) 0px,
+ var(--border) 4px,
+ transparent 4px,
+ transparent 8px
+ );
+}
+
+.exp-card-header {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: flex-start;
+ gap: 12px 24px;
+}
+
+.exp-card-title {
+ min-width: 0;
+}
+
+.exp-title-with-logo {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ min-width: 0;
+}
+
+.exp-logo {
+ height: 44px;
+ width: auto;
+ max-width: 180px;
+ object-fit: contain;
+ flex-shrink: 0;
+}
+
+.exp-logo.logo-hark {
+ height: 30px;
+ max-width: 140px;
+}
+
+.exp-logo.logo-invert {
+ filter: brightness(0) invert(1);
+ opacity: 0.9;
+}
+
+.exp-title-text {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+ min-width: 0;
+}
+
+.exp-title-text h2 {
+ font-size: var(--fs-22);
+ font-weight: 500;
+ color: var(--accent);
+ margin: 0;
+ line-height: 1.1;
+}
+
+.exp-role {
+ display: inline-flex;
+ align-items: center;
+ color: var(--accent);
+ font-size: var(--fs-15);
+ font-weight: 500;
+ white-space: nowrap;
+ line-height: 1.2;
+}
+
+.exp-role::before {
+ content: "/";
+ color: var(--yellow);
+ margin-right: 4px;
+}
+
+.exp-role::after {
+ content: "";
+}
+
+.exp-title-with-logo .exp-role {
+ font-size: var(--fs-16);
+ line-height: 1.1;
+ margin-top: 22px;
+}
+
+.exp-logo.logo-hark + .exp-role {
+ margin-top: 15px;
+}
+
+.exp-card-meta {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 3px;
+ font-size: var(--fs-14);
+ text-align: right;
+ white-space: nowrap;
+}
+
+.exp-duration {
+ color: var(--fg-dim);
+}
+
+.exp-card-tech {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-bottom: 20px;
+}
+
+.exp-card-body {
+ color: var(--fg);
+ line-height: 1.7;
+}
+
+.exp-card-body p {
+ margin-bottom: 12px;
+}
+
+.exp-card-images {
+ display: flex;
+ gap: 12px;
+ margin-top: 12px;
+ padding-top: 12px;
+ border-top: 1px solid var(--border);
+ max-height: 200px;
+ overflow: hidden;
+}
+
+.exp-image-hero {
+ max-width: 280px;
+ height: 100%;
+ border-radius: 4px;
+ border: 1px solid var(--border);
+ object-fit: cover;
+ cursor: pointer;
+}
+
+.exp-image-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ max-width: 400px;
+}
+
+.exp-image-thumb {
+ width: calc(50% - 4px);
+ height: 86px;
+ border-radius: 4px;
+ border: 1px solid var(--border);
+ object-fit: cover;
+ cursor: pointer;
+}
+
+.external-link {
+ color: var(--light-blue);
+ font-size: var(--fs-14);
+ max-width: 36ch;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+@media (max-width: 768px) {
+ .experience-layout {
+ padding: 16px;
+ gap: 12px;
+ }
+
+ .exp-timeline {
+ display: none;
+ }
+
+ .exp-card {
+ padding: 16px;
+ }
+
+ .experience-detail {
+ padding: 4px 14px 4px 0;
+ }
+
+ .exp-card-header {
+ grid-template-columns: 1fr;
+ gap: 10px;
+ }
+
+ .exp-title-with-logo {
+ align-items: flex-start;
+ }
+
+ .exp-logo {
+ height: 38px;
+ max-width: 150px;
+ margin-top: 0;
+ }
+
+ .exp-logo.logo-hark {
+ height: 26px;
+ max-width: 120px;
+ }
+
+ .exp-title-with-logo .exp-role {
+ margin-top: 19px;
+ }
+
+ .exp-logo.logo-hark + .exp-role {
+ margin-top: 13px;
+ }
+
+ .exp-card-meta {
+ align-items: flex-start;
+ text-align: left;
+ white-space: normal;
+ }
+
+ .exp-card-images {
+ flex-direction: column;
+ max-height: none;
+ overflow: visible;
+ }
+
+ .exp-image-hero {
+ max-width: 100%;
+ height: auto;
+ max-height: 200px;
+ }
+
+ .exp-image-thumb {
+ width: calc(33.33% - 6px);
+ height: 80px;
+ }
+}
diff --git a/internal/view/styles/footer.css b/internal/view/styles/footer.css
@@ -0,0 +1,29 @@
+#statusbar {
+ display: flex;
+ align-items: center;
+ background: var(--bg-alt);
+ border-top: 1px solid var(--border);
+ padding: 0 12px;
+ height: 24px;
+ font-size: var(--fs-12);
+ position: sticky;
+ bottom: 0;
+ z-index: 100;
+}
+
+#statusbar .time {
+ color: var(--cyan);
+}
+
+#statusbar .spacer {
+ flex: 1;
+}
+
+#statusbar .wakatime {
+ color: var(--magenta);
+ text-decoration: none;
+}
+
+#statusbar .wakatime:hover {
+ opacity: 0.8;
+}
diff --git a/internal/view/styles/header.css b/internal/view/styles/header.css
@@ -0,0 +1,107 @@
+#tabs {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ background: var(--bg-alt);
+ border-bottom: 1px solid var(--border);
+ padding: 0 12px;
+ height: 36px;
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ user-select: none;
+}
+
+#tabs * {
+ user-select: none;
+}
+
+.tab-nav {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.tab {
+ padding: 6px 12px;
+ color: var(--fg-dim);
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition: color 0.15s, border-color 0.15s;
+}
+
+.tab:hover {
+ color: var(--fg);
+}
+
+.tab.active {
+ color: var(--accent);
+ border-bottom-color: var(--accent);
+}
+
+.tab-spacer {
+ flex: 1;
+}
+
+.status {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--fg-dim);
+ font-size: var(--fs-11);
+ margin-right: 8px;
+}
+
+.dpad {
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1px;
+ font-size: var(--fs-8);
+ line-height: 1;
+}
+
+.dpad-row {
+ display: flex;
+ gap: 1px;
+}
+
+.dpad-key {
+ width: 12px;
+ height: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--border);
+ border-radius: 2px;
+ color: var(--fg);
+}
+
+@media (max-width: 768px) {
+ #tabs {
+ flex-wrap: wrap;
+ height: auto;
+ min-height: 36px;
+ padding: 8px 12px;
+ gap: 8px;
+ }
+
+ .tab-nav {
+ width: 100%;
+ justify-content: space-between;
+ gap: 0;
+ }
+
+ .tab {
+ padding: 6px 8px;
+ font-size: var(--fs-12);
+ flex: 1;
+ text-align: center;
+ }
+
+ .tab-spacer,
+ .status,
+ .tab-num {
+ display: none;
+ }
+}
diff --git a/internal/view/styles/layout.css b/internal/view/styles/layout.css
@@ -0,0 +1,440 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --bg: #0d0d0d;
+ --bg-alt: #161616;
+ --fg: #c0c0c0;
+ --fg-dim: #606060;
+ --accent: #d7af5f;
+ --accent-dim: #a68a3d;
+ --cyan: #5fafaf;
+ --blue: #5f87d7;
+ --light-blue: #5fafff;
+ --yellow: #d7af5f;
+ --magenta: #af5faf;
+ --red: #d75f5f;
+ --green: #5faf5f;
+ --border: #333;
+ --selection: #3d3520;
+
+ --type-1: clamp(9px, 0.24vw + 8px, 11px);
+ --type-2: clamp(10px, 0.28vw + 8.8px, 12px);
+ --type-3: clamp(11px, 0.32vw + 9.6px, 13px);
+ --type-4: clamp(12px, 0.36vw + 10.4px, 14px);
+ --type-5: clamp(13px, 0.45vw + 11.4px, 16px);
+ --type-6: clamp(14px, 0.5vw + 11.8px, 17px);
+ --type-7: clamp(16px, 0.75vw + 13.6px, 22px);
+ --type-8: clamp(22px, 1.4vw + 16px, 32px);
+
+ --fs-8: var(--type-1);
+ --fs-9: var(--type-1);
+ --fs-10: var(--type-1);
+ --fs-11: var(--type-1);
+ --fs-12: var(--type-2);
+ --fs-13: var(--type-3);
+ --fs-14: var(--type-4);
+ --fs-15: var(--type-5);
+ --fs-16: var(--type-5);
+ --fs-17: var(--type-6);
+ --fs-19: var(--type-6);
+ --fs-20: var(--type-7);
+ --fs-22: var(--type-7);
+ --fs-24: var(--type-7);
+ --fs-28: var(--type-8);
+ --fs-32: var(--type-8);
+}
+
+@font-face {
+ font-family: "JetBrains Mono";
+ src: url("https://cdn.jsdelivr.net/npm/@fontsource/jetbrains-mono@5.0.18/files/jetbrains-mono-latin-400-normal.woff2")
+ format("woff2");
+ font-weight: 400;
+}
+
+body {
+ background: var(--bg);
+ color: var(--fg);
+ font-family: "JetBrains Mono", "SF Mono", "Fira Code", monospace;
+ font-size: var(--fs-14);
+ line-height: 1.5;
+ min-height: 100vh;
+ overflow: hidden;
+}
+
+* {
+ scrollbar-width: auto;
+ scrollbar-color: var(--accent-dim) var(--bg-alt);
+}
+
+*::-webkit-scrollbar {
+ width: 12px;
+ height: 12px;
+}
+
+*::-webkit-scrollbar-track {
+ background: var(--bg-alt);
+ border-left: 1px solid var(--border);
+ border-right: 1px solid var(--border);
+}
+
+*::-webkit-scrollbar-track:horizontal {
+ background: var(--bg-alt);
+ border-top: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+}
+
+*::-webkit-scrollbar-thumb {
+ background: var(--accent-dim);
+ border-radius: 0;
+ border: 1px solid var(--accent);
+ box-shadow: inset 0 0 0 1px var(--bg-alt);
+ min-height: 28px;
+}
+
+*::-webkit-scrollbar-thumb:hover {
+ background: var(--accent);
+}
+
+*::-webkit-scrollbar-thumb:active {
+ background: var(--yellow);
+}
+
+*::-webkit-scrollbar-corner {
+ background: var(--bg-alt);
+}
+
+a {
+ color: var(--light-blue);
+ text-decoration: none;
+}
+
+a:hover {
+ opacity: 0.8;
+}
+
+.color-dim {
+ color: var(--fg-dim);
+}
+
+#app {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ opacity: 0;
+}
+
+#app.ready {
+ opacity: 1;
+}
+
+#content {
+ flex: 1;
+ overflow: hidden;
+}
+
+.page {
+ display: flex;
+ height: 100%;
+}
+
+.tag {
+ border: none;
+ padding: 2px 8px;
+ font-size: var(--fs-12);
+ color: var(--bg);
+ background: var(--fg-dim);
+}
+
+.tag.lang-zig {
+ background: #f7a41d;
+}
+
+.tag.lang-rust {
+ background: #dea584;
+}
+
+.tag.lang-c {
+ background: #a8b1c2;
+}
+
+.tag.lang-go {
+ background: #00add8;
+}
+
+.tag.lang-ts {
+ background: #3178c6;
+ color: #fff;
+}
+
+.tag.lang-python {
+ background: #f4c542;
+ color: #0d0d0d;
+}
+
+.tag.tool-zed {
+ background: #084ccf;
+ color: #fff;
+}
+
+.tag.tool-neovim {
+ background: #57a143;
+}
+
+.tag.tool-opencode {
+ background: #af5faf;
+ color: #fff;
+}
+
+.tag.tool-linux {
+ background: #f7c94c;
+}
+
+.tag.tool-fish {
+ background: #d78700;
+}
+
+.tag.tool-ghostty {
+ background: #8b5cf6;
+ color: #fff;
+}
+
+.tag.interest-lowlevel {
+ background: #d75f5f;
+ color: #fff;
+}
+
+.tag.interest-tui {
+ background: #5faf5f;
+ color: #0d0d0d;
+}
+
+.tag.interest-embedded {
+ background: #5fafaf;
+ color: #0d0d0d;
+}
+
+.tag.interest-unix {
+ background: #d7af5f;
+ color: #0d0d0d;
+}
+
+.tag.tech-temporal {
+ background: #4c35c9;
+ color: #fff;
+}
+
+.tag.tech-postgres {
+ background: #336791;
+ color: #fff;
+}
+
+.tag.tech-pytorch {
+ background: #ee4c2c;
+ color: #fff;
+}
+
+.tag.tech-cuda {
+ background: #76b900;
+}
+
+.tag.tech-docker {
+ background: #2496ed;
+ color: #fff;
+}
+
+.tag.tech-kafka {
+ background: #4c35c9;
+ color: #fff;
+}
+
+.tag.tech-rn {
+ background: #61dafb;
+}
+
+.tag.tech-supabase {
+ background: #3ecf8e;
+}
+
+.tag.tech-rest {
+ background: #6b8e23;
+}
+
+#boot-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: var(--bg);
+ z-index: 9999;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+#boot-overlay.fade-out {
+ animation: bootFadeOut 0.3s ease-out forwards;
+}
+
+.boot-terminal {
+ text-align: center;
+}
+
+.boot-text {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.boot-title {
+ font-family: "JetBrains Mono", "SF Mono", monospace;
+ font-size: var(--fs-32);
+ font-weight: 600;
+ color: var(--accent);
+ letter-spacing: -0.5px;
+}
+
+.boot-subtitle {
+ font-family: "JetBrains Mono", "SF Mono", monospace;
+ font-size: var(--fs-14);
+ color: var(--fg-dim);
+ letter-spacing: 0.5px;
+}
+
+@keyframes bootFadeOut {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ }
+}
+
+#app.boot-reveal {
+ animation: revealContent 0.5s ease-out;
+}
+
+@keyframes revealContent {
+ 0% {
+ opacity: 0;
+ transform: translateY(15px);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+#help-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(0, 0, 0, 0.8);
+ z-index: 9998;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.2s, visibility 0.2s;
+}
+
+#help-overlay.visible {
+ opacity: 1;
+ visibility: visible;
+}
+
+.help-modal {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ padding: 20px 24px;
+ min-width: 280px;
+ max-width: 90vw;
+}
+
+.help-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid var(--border);
+}
+
+.help-title {
+ color: var(--accent);
+ font-weight: 600;
+}
+
+.help-close {
+ color: var(--fg-dim);
+ font-size: var(--fs-11);
+}
+
+.help-content {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.help-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 24px;
+}
+
+.help-key {
+ color: var(--cyan);
+ font-family: "JetBrains Mono", monospace;
+ font-size: var(--fs-12);
+ min-width: 60px;
+}
+
+.help-desc {
+ color: var(--fg);
+ font-size: var(--fs-13);
+}
+
+@media (max-width: 768px) {
+ body {
+ font-size: var(--fs-13);
+ overflow-y: auto;
+ }
+
+ #app {
+ min-height: 100vh;
+ height: auto;
+ }
+
+ #content {
+ overflow: visible;
+ }
+
+ .page {
+ height: auto;
+ min-height: 100vh;
+ }
+
+ .boot-title {
+ font-size: var(--fs-24);
+ }
+
+ .boot-subtitle {
+ font-size: var(--fs-12);
+ }
+
+ .help-modal {
+ padding: 16px 20px;
+ min-width: 260px;
+ }
+}
diff --git a/internal/view/styles/lightbox.css b/internal/view/styles/lightbox.css
@@ -0,0 +1,54 @@
+.lightbox {
+ position: fixed;
+ inset: 0;
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.lightbox.hidden {
+ display: none;
+}
+
+.lightbox-overlay {
+ position: absolute;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.9);
+ cursor: pointer;
+}
+
+.lightbox-content {
+ position: relative;
+ max-width: 90vw;
+ max-height: 90vh;
+ z-index: 1;
+}
+
+.lightbox-image {
+ max-width: 100%;
+ max-height: 90vh;
+ border-radius: 4px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
+}
+
+.lightbox-close {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ background: rgba(0, 0, 0, 0.7);
+ border: 1px solid var(--border);
+ color: var(--fg);
+ font-family: "JetBrains Mono", monospace;
+ font-size: var(--fs-12);
+ cursor: pointer;
+ padding: 4px 8px;
+ line-height: 1;
+ transition: color 0.15s, background 0.15s;
+ z-index: 10;
+}
+
+.lightbox-close:hover {
+ color: var(--accent);
+ background: rgba(0, 0, 0, 0.9);
+}
diff --git a/internal/view/styles/project_detail.css b/internal/view/styles/project_detail.css
@@ -0,0 +1,129 @@
+.detail-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+
+.detail-path {
+ min-width: 0;
+ font-size: var(--fs-19);
+ font-weight: 400;
+}
+
+.detail-path-text {
+ color: var(--accent);
+ overflow-wrap: anywhere;
+}
+
+.tech-stack {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-bottom: 16px;
+}
+
+.external-link {
+ color: var(--light-blue);
+ font-size: var(--fs-12);
+}
+
+.readme-content {
+ flex: 0 0 auto;
+ min-height: auto;
+ overflow: visible;
+ font-size: var(--fs-14);
+ line-height: 1.6;
+ color: var(--fg);
+ margin-top: 16px;
+ padding-top: 16px;
+ padding-right: 0;
+ border-top: 1px solid var(--border);
+}
+
+.readme-content p {
+ margin-bottom: 12px;
+}
+
+.readme-content h1,
+.readme-content h2,
+.readme-content h3 {
+ color: var(--accent);
+ margin: 16px 0 8px;
+}
+
+.readme-content pre {
+ display: block;
+ background: var(--bg-alt);
+ border: 1px solid var(--border);
+ padding: 12px;
+ margin: 12px 0;
+ overflow-x: auto;
+ font-size: var(--fs-12);
+}
+
+.readme-content code {
+ background: var(--bg-alt);
+ padding: 2px 6px;
+ font-size: var(--fs-12);
+}
+
+.readme-content pre code {
+ background: transparent;
+ padding: 0;
+ font-size: inherit;
+}
+
+.readme-content a {
+ color: var(--light-blue);
+}
+
+.readme-content img {
+ display: block;
+ max-width: 75%;
+ height: auto;
+ margin: 12px auto;
+ border: 1px solid var(--border);
+}
+
+.readme-content ul,
+.readme-content ol {
+ margin: 8px 0;
+ padding-left: 20px;
+}
+
+.readme-content table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 12px 0;
+ font-size: var(--fs-12);
+}
+
+.readme-content th,
+.readme-content td {
+ border: 1px solid var(--border);
+ padding: 8px 12px;
+ text-align: left;
+}
+
+.readme-content th {
+ background: var(--bg-alt);
+ color: var(--accent);
+ font-weight: 500;
+}
+
+@media (max-width: 768px) {
+ .detail-header {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .detail-path {
+ font-size: var(--fs-17);
+ }
+
+ .readme-content img {
+ max-width: 100%;
+ }
+}
diff --git a/internal/view/styles/projects.css b/internal/view/styles/projects.css
@@ -0,0 +1,91 @@
+#projects {
+ display: flex;
+ width: 100%;
+}
+
+aside.list-view {
+ width: 300px;
+ flex-shrink: 0;
+ border-right: 1px solid var(--border);
+ overflow-y: auto;
+}
+
+article.detail-view {
+ flex: 1;
+ min-width: 0;
+ padding: 24px 32px;
+ overflow-x: hidden;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+}
+
+#project-detail {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.list-item {
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border);
+ cursor: pointer;
+ transition: background 0.1s;
+}
+
+.list-item:hover {
+ background: var(--bg-alt);
+}
+
+.list-item.selected {
+ background: var(--selection);
+ border-left: 3px solid var(--accent);
+}
+
+.list-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2px;
+}
+
+.list-title {
+ font-weight: 600;
+ color: var(--fg);
+}
+
+.list-meta {
+ font-size: var(--fs-11);
+ color: var(--fg-dim);
+}
+
+.list-subtitle {
+ font-size: var(--fs-12);
+ color: var(--fg-dim);
+}
+
+.detail-content {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ min-height: 0;
+}
+
+@media (max-width: 768px) {
+ #projects {
+ flex-direction: column;
+ }
+
+ aside.list-view {
+ width: 100%;
+ border-right: none;
+ border-bottom: 1px solid var(--border);
+ max-height: 200px;
+ }
+
+ article.detail-view {
+ padding: 16px;
+ overflow-y: auto;
+ }
+}
diff --git a/internal/view/templates/layout.gohtml b/internal/view/templates/layout.gohtml
@@ -0,0 +1,63 @@
+{{define "layout"}}
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>{{.Title}} | {{.About.Name}}</title>
+ <meta name="description" content="{{.Description}}">
+ <link rel="canonical" href="{{tabURL .ActiveTab}}">
+ <link rel="alternate" hreflang="en" href="{{tabURL .ActiveTab}}">
+ <link rel="alternate" hreflang="x-default" href="{{tabURL .ActiveTab}}">
+ <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin="anonymous">
+ <link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
+ <link rel="preload" href="https://cdn.jsdelivr.net/npm/@fontsource/jetbrains-mono@5.0.18/files/jetbrains-mono-latin-400-normal.woff2" as="font" type="font/woff2" crossorigin="anonymous">
+ <meta property="og:type" content="website">
+ <meta property="og:url" content="{{tabURL .ActiveTab}}">
+ <meta property="og:title" content="{{.Title}} | {{.About.Name}}">
+ <meta property="og:description" content="{{.Description}}">
+ <meta property="og:image" content="https://www.prime8.dev/static/me.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="{{.About.Name}}">
+ <meta property="og:locale" content="en_US">
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:url" content="{{tabURL .ActiveTab}}">
+ <meta name="twitter:title" content="{{.Title}} | {{.About.Name}}">
+ <meta name="twitter:description" content="{{.Description}}">
+ <meta name="twitter:image" content="https://www.prime8.dev/static/me.jpg">
+ <meta name="author" content="{{.About.Name}}">
+ <meta name="robots" content="index, follow">
+ <meta name="theme-color" content="#0d0d0d">
+ <link rel="icon" type="image/png" href="/static/favicon.png">
+ <link rel="apple-touch-icon" href="/static/favicon.png">
+ <link rel="manifest" href="/static/site.webmanifest">
+ <script defer src="https://metrics.pufferfish-kitefin.ts.net/script.js" data-website-id="d3032378-7959-4411-92ec-f67e20399fe4"></script>
+ <script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>
+ <style>{{inlineCSS "layout.css"}}</style>
+</head>
+<body>
+ <div id="app" data-boot-title="{{.About.Name}}" data-boot-subtitle="www.prime8.dev">
+ {{template "header" .}}
+ <main id="content">
+ {{if eq .ContentTemplate "about"}}
+ {{template "about" .}}
+ {{else if eq .ContentTemplate "projects"}}
+ {{template "projects" .}}
+ {{else if eq .ContentTemplate "experience"}}
+ {{template "experience" .}}
+ {{else if eq .ContentTemplate "blog"}}
+ {{template "blog" .}}
+ {{end}}
+ </main>
+ {{template "footer" .}}
+ </div>
+
+ {{template "lightbox" .}}
+
+ <script src="/static/app.js" defer></script>
+ <script type="application/ld+json">{"@context":"https://schema.org","@type":"Person","name":"{{.About.Name}}"}</script>
+ <script type="application/ld+json">{"@context":"https://schema.org","@type":"WebSite","name":"{{.About.Name}}","url":"https://www.prime8.dev"}</script>
+</body>
+</html>
+{{end}}
diff --git a/internal/view/templates/partials/about.gohtml b/internal/view/templates/partials/about.gohtml
@@ -0,0 +1,70 @@
+{{define "about"}}
+<section id="about" class="page active">
+ <div class="about-layout">
+ <div class="about-left">
+ <img class="profile-image" src="/static/me.jpg" alt="damian myrda - software engineer and high school senior from chicago" loading="eager">
+ <div class="contact-box">
+ <div class="shell-prompt"><span class="shell-user">{{.About.Handle}}</span>@<span class="shell-host">prime8</span>:<span class="shell-path">~</span><span class="shell-dollar">$</span> <span class="shell-cmd">cat contact</span></div>
+ <div class="contact-item"><span class="label">email</span><a href="mailto:damian@prime8.dev">damian@prime8.dev</a></div>
+ <div class="contact-item"><span class="label">gitlab</span><a href="https://git.prime8.dev/moncheeta" target="_blank" rel="noopener noreferrer me">git.prime8.dev/moncheeta</a></div>
+ <div class="contact-item"><span class="label">linkedin</span><a href="https://linkedin.com/in/damian-myrda" target="_blank" rel="noopener noreferrer me">linkedin.com/in/damian-myrda</a></div>
+ </div>
+ </div>
+
+ <div class="about-right">
+ <div class="section">
+ <div class="shell-prompt"><span class="shell-user">{{.About.Handle}}</span>@<span class="shell-host">prime8</span>:<span class="shell-path">~</span><span class="shell-dollar">$</span> <span class="shell-cmd">whoami</span></div>
+ <pre class="name-ascii" aria-label="{{.About.Name}}">{{.AboutNameASCII}}</pre>
+ <p class="bio">{{.About.Bio}}</p>
+ </div>
+
+ <div class="section">
+ <div class="shell-prompt"><span class="shell-user">{{.About.Handle}}</span>@<span class="shell-host">prime8</span>:<span class="shell-path">~</span><span class="shell-dollar">$</span> <span class="shell-cmd">cat info</span></div>
+ <div class="info-columns">
+ <div class="info-column">
+ <div class="info-row"><span class="label">location:</span>{{.About.Location}}</div>
+ <div class="info-row"><span class="label">school:</span>{{.About.School}}</div>
+ <div class="info-row"><span class="label">graduating:</span>{{.About.Graduating}}</div>
+ </div>
+ {{with .CurrentJob}}
+ <div class="info-column">
+ <div class="info-row"><span class="label">company:</span><a href="/experience/{{.ID}}" hx-get="/experience/{{.ID}}" hx-target="#content" hx-push-url="true">{{.Company}}</a></div>
+ <div class="info-row"><span class="label">position:</span>{{.Role}}</div>
+ </div>
+ {{end}}
+ </div>
+ </div>
+
+ <div class="section">
+ <div class="shell-prompt"><span class="shell-user">{{.About.Handle}}</span>@<span class="shell-host">prime8</span>:<span class="shell-path">~</span><span class="shell-dollar">$</span> <span class="shell-cmd">ls interests/</span></div>
+ <div class="tags">
+ {{range .About.Interests}}<span class="tag {{tagClass .}}">{{.}}</span>{{end}}
+ </div>
+ </div>
+
+ <div class="section">
+ <div class="shell-prompt"><span class="shell-user">{{.About.Handle}}</span>@<span class="shell-host">prime8</span>:<span class="shell-path">~</span><span class="shell-dollar">$</span> <span class="shell-cmd">ls tools/</span></div>
+ <div class="tags">
+ {{range .About.Tools}}<span class="tag {{tagClass .}}">{{.}}</span>{{end}}
+ </div>
+ </div>
+
+ <div class="section">
+ <div class="shell-prompt"><span class="shell-user">{{.About.Handle}}</span>@<span class="shell-host">prime8</span>:<span class="shell-path">~</span><span class="shell-dollar">$</span> <span class="shell-cmd">ls languages/</span></div>
+ <div class="tags">
+ {{range .About.Languages}}<span class="tag {{tagClass .}}">{{.}}</span>{{end}}
+ </div>
+ </div>
+
+ <div class="section" id="activity-section">
+ <div class="shell-prompt"><span class="shell-user">{{.About.Handle}}</span>@<span class="shell-host">prime8</span>:<span class="shell-path">~</span><span class="shell-dollar">$</span> <span class="shell-cmd">wakatime --week</span></div>
+ <div class="weekly-stats" id="activity-stats">
+ <div class="loading-text">loading stats...</div>
+ </div>
+ </div>
+ </div>
+ </div>
+</section>
+
+<style>{{inlineCSS "about.css"}}</style>
+{{end}}
diff --git a/internal/view/templates/partials/blog.gohtml b/internal/view/templates/partials/blog.gohtml
@@ -0,0 +1,46 @@
+{{define "blog"}}
+<section id="blog" class="page">
+ {{if eq (len .Blogs) 0}}
+ <div class="blog-empty">
+ <p class="color-dim">~/blog is empty</p>
+ <p class="color-dim">check back soon</p>
+ </div>
+ {{else}}
+ <div id="blog-list-view" class="blog-list-view{{if .Blog}} hidden{{end}}">
+ <div class="blog-fs-shell">
+ <div class="blog-fs-header">~/blog $ ls -l</div>
+ <div class="blog-list-columns">
+ <span class="col-mode">mode</span>
+ <span>date</span>
+ <span>name</span>
+ <span class="col-desc">desc</span>
+ <span class="col-arrow"></span>
+ </div>
+ <div class="blog-list-container">
+ {{range $i, $blog := .Blogs}}
+ <div class="blog-list-item{{if $.SelectedBlogID}}{{if eq $blog.ID $.SelectedBlogID}} selected{{end}}{{else}}{{if eq $i 0}} selected{{end}}{{end}}" data-index="{{$i}}" data-id="{{$blog.ID}}" hx-get="/api/blog/{{$blog.ID}}" hx-target="#blog-post-content" hx-swap="innerHTML">
+ <span class="blog-list-mode">-rw-r--r--</span>
+ <span class="blog-list-date">{{$blog.Date.Format "2006-01-02"}}</span>
+ <span class="blog-list-name">
+ <span class="blog-list-file">{{$blog.ID}}.md</span>
+ <span class="blog-list-title">{{$blog.Title}}</span>
+ </span>
+ <span class="blog-list-desc" title="{{$blog.Description}}">{{$blog.Description}}</span>
+ <span class="blog-list-arrow">-></span>
+ </div>
+ {{end}}
+ </div>
+ </div>
+ </div>
+
+ <div id="blog-post-view" class="blog-post-view{{if not .Blog}} hidden{{end}}">
+ <div class="blog-post-container">
+ <button class="blog-back-btn">cd ..</button>
+ <div id="blog-post-content">{{if .Blog}}{{template "blog_detail" .}}{{end}}</div>
+ </div>
+ </div>
+ {{end}}
+</section>
+
+<style>{{inlineCSS "blog.css"}}</style>
+{{end}}
diff --git a/internal/view/templates/partials/blog_detail.gohtml b/internal/view/templates/partials/blog_detail.gohtml
@@ -0,0 +1,28 @@
+{{define "blog_detail"}}
+{{if .Blog}}
+<article class="blog-article" data-id="{{.Blog.ID}}" data-blog-id="{{.Blog.ID}}">
+ <header class="blog-article-header">
+ <div class="blog-article-header-top">
+ <h1 class="blog-article-title">~/blog/{{.Blog.ID}}.md</h1>
+ <button class="blog-share-btn blog-share-btn-desktop" data-blog-id="{{.Blog.ID}}" data-blog-title="{{.Blog.ID}}.md">[Share]</button>
+ </div>
+ <div class="blog-article-meta">
+ <time class="blog-article-date" datetime="{{.Blog.Date.Format "2006-01-02"}}">{{.Blog.Date.Format "2006-01-02"}}</time>
+ <button class="blog-share-btn blog-share-btn-mobile" data-blog-id="{{.Blog.ID}}" data-blog-title="{{.Blog.ID}}.md">[Share]</button>
+ </div>
+ </header>
+
+ <div class="blog-article-content" data-blog-id="{{.Blog.ID}}">
+ {{if .Blog.URL}}
+ <a class="external-link" href="{{.Blog.URL}}" target="_blank" rel="noopener noreferrer">Read on external site -></a>
+ {{else if .Blog.Content}}
+ {{toHTML .Blog.Content}}
+ {{else}}
+ <p class="color-dim">Content not available</p>
+ {{end}}
+ </div>
+</article>
+
+<style>{{inlineCSS "blog_detail.css"}}</style>
+{{end}}
+{{end}}
diff --git a/internal/view/templates/partials/experience.gohtml b/internal/view/templates/partials/experience.gohtml
@@ -0,0 +1,65 @@
+{{define "experience"}}
+<section id="experience" class="page">
+ <article class="experience-layout">
+ <div class="experience-detail">
+ {{range $index, $exp := .Experience}}
+ {{$hasYear := durationHasYear $exp.StartDate $exp.EndDate $exp.Current}}
+ <div class="exp-row" data-index="{{$index}}" data-id="{{$exp.ID}}">
+ <div class="exp-timeline{{if $.SelectedExperienceID}}{{if eq $exp.ID $.SelectedExperienceID}} active{{end}}{{else}}{{if eq $index 0}} active{{end}}{{end}}{{if $hasYear}} has-end-dot{{end}}" data-index="{{$index}}">
+ <span class="timeline-dot"></span>
+ <span class="timeline-year">[{{timelineYear $exp.EndDate $exp.Current}}]</span>
+ <div class="timeline-line"></div>
+ {{if $hasYear}}<span class="timeline-dot timeline-dot-end"></span>{{end}}
+ </div>
+
+ <div class="exp-card{{if $.SelectedExperienceID}}{{if eq $exp.ID $.SelectedExperienceID}} selected{{end}}{{else}}{{if eq $index 0}} selected{{end}}{{end}}" data-index="{{$index}}" data-id="{{$exp.ID}}">
+ <div class="exp-card-header">
+ <div class="exp-card-title">
+ {{if $exp.Logo}}
+ <div class="exp-title-with-logo">
+ <img src="{{$exp.Logo}}" alt="{{$exp.Company}} logo" class="exp-logo {{$exp.LogoFilter}}">
+ <span class="exp-role">{{$exp.Role}}</span>
+ </div>
+ {{else}}
+ <div class="exp-title-text">
+ <h2>{{$exp.Company}}</h2>
+ <span class="exp-role">{{$exp.Role}}</span>
+ </div>
+ {{end}}
+ </div>
+ <div class="exp-card-meta">
+ <span class="exp-dates">{{formatMonthYear $exp.StartDate}} -> {{formatEndDate $exp.EndDate $exp.Current}}</span>
+ <span class="exp-duration">{{formatDuration $exp.StartDate $exp.EndDate $exp.Current}}</span>
+ {{if $exp.URL}}<a class="external-link" href="{{$exp.URL}}" target="_blank" rel="noopener noreferrer">{{$exp.URL}} -></a>{{end}}
+ </div>
+ </div>
+
+ {{if gt (len $exp.Tech) 0}}
+ <div class="exp-card-tech">{{range $exp.Tech}}<span class="tag {{tagClass .}}">{{.}}</span>{{end}}</div>
+ {{end}}
+
+ <div class="exp-card-body">{{range $exp.Description}}<p>{{.}}</p>{{end}}</div>
+
+ {{if gt (len $exp.Images) 0}}
+ <div class="exp-card-images">
+ <img src="{{index $exp.Images 0}}" alt="{{$exp.Company}} workplace" class="exp-image-hero" loading="lazy">
+ {{if gt (len $exp.Images) 1}}
+ <div class="exp-image-grid">
+ {{range $imgIndex, $img := $exp.Images}}
+ {{if gt $imgIndex 0}}
+ <img src="{{$img}}" alt="{{$exp.Company}} image" class="exp-image-thumb" loading="lazy">
+ {{end}}
+ {{end}}
+ </div>
+ {{end}}
+ </div>
+ {{end}}
+ </div>
+ </div>
+ {{end}}
+ </div>
+ </article>
+</section>
+
+<style>{{inlineCSS "experience.css"}}</style>
+{{end}}
diff --git a/internal/view/templates/partials/footer.gohtml b/internal/view/templates/partials/footer.gohtml
@@ -0,0 +1,9 @@
+{{define "footer"}}
+<footer id="statusbar">
+ <span class="time" id="status-time"></span>
+ <span class="spacer"></span>
+ <a class="wakatime" id="wakatime-link" href="https://wakatime.com/@{{.About.Handle}}" target="_blank" rel="noopener noreferrer">Loading...</a>
+</footer>
+
+<style>{{inlineCSS "footer.css"}}</style>
+{{end}}
diff --git a/internal/view/templates/partials/header.gohtml b/internal/view/templates/partials/header.gohtml
@@ -0,0 +1,21 @@
+{{define "header"}}
+<header id="tabs" data-active-tab="{{.ActiveTab}}" data-tabs="about,projects,experience,blog">
+ <nav class="tab-nav" aria-label="main navigation">
+ {{range .Tabs}}
+ <a href="{{tabPath .ID}}" class="tab{{if eq .ID $.ActiveTab}} active{{end}}" data-tab="{{.ID}}" data-label="{{.ID}}" hx-get="{{tabPath .ID}}" hx-target="#content" hx-push-url="true">
+ <span class="tab-num">[{{.Num}}] </span>{{.ID}}
+ </a>
+ {{end}}
+ </nav>
+ <div class="tab-spacer"></div>
+ <div class="status" id="header-status">
+ <span class="dpad">
+ <span class="dpad-row"><span class="dpad-key">▲</span></span>
+ <span class="dpad-row"><span class="dpad-key">◀</span><span class="dpad-key">▼</span><span class="dpad-key">▶</span></span>
+ </span>
+ <span class="navigate"> navigate · tab cycle · ? help</span>
+ </div>
+</header>
+
+<style>{{inlineCSS "header.css"}}</style>
+{{end}}
diff --git a/internal/view/templates/partials/lightbox.gohtml b/internal/view/templates/partials/lightbox.gohtml
@@ -0,0 +1,11 @@
+{{define "lightbox"}}
+<div id="image-lightbox" class="lightbox hidden">
+ <div class="lightbox-overlay"></div>
+ <div class="lightbox-content">
+ <img src="" alt="" class="lightbox-image">
+ <button class="lightbox-close">[X]</button>
+ </div>
+</div>
+
+<style>{{inlineCSS "lightbox.css"}}</style>
+{{end}}
diff --git a/internal/view/templates/partials/project_detail.gohtml b/internal/view/templates/partials/project_detail.gohtml
@@ -0,0 +1,18 @@
+{{define "project_detail"}}
+{{if .Project}}
+<div class="detail-content" data-id="{{.Project.ID}}">
+ <div class="detail-header">
+ <h2 class="detail-path"><span class="detail-path-text">~/projects/{{.Project.Name}}</span></h2>
+ <a class="external-link" href="{{.Project.URL}}" target="_blank" rel="noopener noreferrer">{{if .ProjectLinkLabel}}{{.ProjectLinkLabel}}{{else}}Outside{{end}} -></a>
+ </div>
+ <div class="tech-stack">
+ {{range .Project.Tech}}<span class="tag {{tagClass .}}">{{.}}</span>{{end}}
+ </div>
+ <div class="readme-content" data-readme="{{.Project.ID}}">
+ {{if .ProjectReadme}}{{.ProjectReadme}}{{else}}<p class="color-dim">README not available</p>{{end}}
+ </div>
+</div>
+
+<style>{{inlineCSS "project_detail.css"}}</style>
+{{end}}
+{{end}}
diff --git a/internal/view/templates/partials/projects.gohtml b/internal/view/templates/partials/projects.gohtml
@@ -0,0 +1,28 @@
+{{define "projects"}}
+<section id="projects" class="page">
+ <aside class="list-view" aria-label="Project list">
+ {{range $i, $p := .Projects}}
+ <div class="list-item{{if $.SelectedProjectID}}{{if eq $p.ID $.SelectedProjectID}} selected{{end}}{{else}}{{if eq $i 0}} selected{{end}}{{end}}" data-index="{{$i}}" data-id="{{$p.ID}}" data-url="{{$p.URL}}" hx-get="/api/projects/{{$p.ID}}" hx-target="#project-detail" hx-swap="innerHTML">
+ <div class="list-header">
+ <span class="list-title">{{$p.Name}}</span>
+ <span class="list-meta">{{techMeta $p.Tech}}</span>
+ </div>
+ <div class="list-subtitle">{{$p.Subtitle}}</div>
+ </div>
+ {{end}}
+ </aside>
+ <article class="detail-view">
+ <div id="project-detail">
+ {{if .Project}}
+ {{template "project_detail" .}}
+ {{else if gt (len .Projects) 0}}
+ <p class="color-dim" data-project-placeholder="true">$ cat README.md</p>
+ {{else}}
+ <p class="color-dim">No projects available.</p>
+ {{end}}
+ </div>
+ </article>
+</section>
+
+<style>{{inlineCSS "projects.css"}}</style>
+{{end}}
diff --git a/package.json b/package.json
@@ -1,40 +0,0 @@
-{
- "name": "prime8dev",
- "private": true,
- "version": "1.0.0",
- "type": "module",
- "scripts": {
- "dev": "astro dev",
- "build": "astro build",
- "preview": "astro preview",
- "lint": "bun eslint .",
- "format": "bun prettier --write ."
- },
- "lint-staged": {
- "*.{ts,astro,js}": [
- "eslint --fix",
- "prettier --write"
- ],
- "*.{json,md,css}": [
- "prettier --write"
- ]
- },
- "dependencies": {
- "astro-bun-adapter": "^1.0.2",
- "astro": "^5.17.2",
- "gray-matter": "^4.0.3",
- "jimp": "^1.6.0",
- "marked": "^17.0.2"
- },
- "devDependencies": {
- "@eslint/js": "^9.39.2",
- "eslint": "^9.39.2",
- "eslint-config-prettier": "^10.1.8",
- "eslint-plugin-astro": "^1.6.0",
- "globals": "^17.3.0",
- "lint-staged": "^16.2.7",
- "prettier": "^3.8.1",
- "prettier-plugin-astro": "^0.14.1",
- "typescript-eslint": "^8.56.0"
- }
-}
diff --git a/src/blogs/building-my-portfolio.md b/src/blogs/building-my-portfolio.md
@@ -1,42 +0,0 @@
----
-id: building-my-portfolio
-title: Building a TUI-Inspired Portfolio
-description: How I built this terminal-style portfolio with Astro
-date: 2026-01-31
-tags:
- - Astro
- - TypeScript
- - TUI
- - Web Development
- - Portfolio
----
-
-Once I got used to the terminal, **I couldn't live without it**. I love the **simplicity** of the interface and the fact that it's **keyboard native** (no need to reach for the mouse). Anything can be reached within a few key presses, which is one of the characteristics that I love.
-
-## The Stack
-
-I built this website using **Astro** using server-side rendering. I tried to keep it as simple as possible, no over-engineering. Don't need WASM, a full-fledged database, or a complex UI framework. Details about me are stored in json files for ease of access (no need to scour the entire project to make a change), and my blogs are stored in a directory of markdown files.
-
-## Design Decisions
-
-Some characteristics that I've observed in terminal user interfaces I've transferred over to my website:
-
-- **Keyboard-first navigation**: `Arrow keys` allows navigation throughout the UI. `Tab` and the number keys (`1-4`) can be used to switch between tabs instantly. `Enter` intuitively opens blog posts and `Escape` closes them.
-- **No stale content**: One thing I don't want to worry about it updating my website every time something changes. This is a design pattern that is followed in the entire project.
- - Project details are fetched from my [self-hosted GitLab instance](https://git.prime8.dev).
- - Blog metadata is taken from the file stats, not a hard coded value.
- - Coding stats are pulled from [my Wakatime account](https://wakatime.com/@moncheeta).
-- **Personality**: One thing I want to make sure of is that this website is unique, but also derives some of its elements from existing terminal applications.
- - Boot animation adds a feeling of life to the website, even though it's entirely static.
- - Top tab bar is an idea taken from the countless GUI applications that have it.
- - Bottom status bar is inspired by many terminal applications that use a similar setup.
-
-These characteristics make this website feel like the terminal applications that I'm in love with, while differentiating it to make it mine.
-
-## Conclusion
-
-Building this portfolio was a chance for me to combine two things **I love**: terminal user interfaces and modern web development. The result is something that feels **personal**, a site that actually reflects how I actually work.
-
-If you're thinking about building your own portfolio, my advice is simple: **make it yours**. Pick a style that exemplifies you and add the features you'd actually want to use.
-
-The source code is available at [git.prime8.dev](https://git.prime8.dev/moncheeta/prime8dev)
diff --git a/src/blogs/index.ts b/src/blogs/index.ts
@@ -1,56 +0,0 @@
-import matter from "gray-matter";
-import { parseMarkdown, removeFirstH1 } from "../utils/markdown";
-
-export interface Blog {
- id: string;
- title: string;
- description: string;
- date: string;
- content?: string;
- url?: string;
- image?: string;
- tags?: string[];
-}
-
-const blogModules = import.meta.glob("./*.md", {
- eager: true,
- query: "?raw",
- import: "default",
-});
-
-export const blogs = Object.values(blogModules)
- .map((content) => parseBlog(content as string))
- .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
-
-function parseBlog(markdown: string): Blog {
- const { data, content } = matter(markdown);
-
- const blog: Partial<Blog> = {
- id: data.id as string | undefined,
- title: data.title as string | undefined,
- description: data.description as string | undefined,
- date:
- data.date instanceof Date
- ? data.date.toISOString().split("T")[0]
- : (data.date as string | undefined),
- url: data.url as string | undefined,
- content: parseMarkdown(removeFirstH1(content)),
- image: data.image as string | undefined,
- tags: data.tags as string[] | undefined,
- };
-
- const missingKeys = ["id", "title", "description", "date"].filter(
- (key) => !blog[key as keyof typeof blog],
- );
- if (missingKeys.length > 0) {
- throw new Error(`Missing keys: ${missingKeys.join(", ")}`);
- }
-
- if (blog.url && blog.content) {
- throw new Error("Blogs cannot have both a URL and content");
- } else if (!blog.url && !blog.content) {
- throw new Error("Blogs must have either a URL or content");
- }
-
- return blog as Blog;
-}
diff --git a/src/config/about.ts b/src/config/about.ts
@@ -1,32 +0,0 @@
-export default interface About {
- name: string;
- handle: string;
- bio: string;
- location: string;
- profession: string;
- profession_short: string;
- languages: string[];
- interests: string[];
- tools: string[];
- school?: string;
- graduating?: string;
-}
-
-export const about = {
- name: "Damian Myrda",
- handle: "moncheeta",
- bio: "Passionate about low-level systems programming, TUI/CLI tooling, and UNIX philosophy. Always learning just like Curious George 🐒",
- location: "Chicago, IL",
- profession: "Software Engineer",
- profession_short: "SWE",
- languages: ["Zig", "Rust", "C", "Go", "TypeScript", "Python"],
- interests: [
- "Low-level systems",
- "TUI/CLI tooling",
- "Embedded",
- "UNIX philosophy",
- ],
- tools: ["Zed (Vim mode)", "OpenCode", "Linux", "Fish"],
- school: "Prospect High School (Senior)",
- graduating: "May 2026",
-} as About;
diff --git a/src/config/contact.ts b/src/config/contact.ts
@@ -1,11 +0,0 @@
-export default interface Contact {
- email: string;
- gitlab: string;
- linkedin: string;
-}
-
-export const contact = {
- email: "damian@prime8.dev",
- gitlab: "https://git.prime8.dev/moncheeta",
- linkedin: "https://linkedin.com/in/damian-myrda",
-} as Contact;
diff --git a/src/config/experience.ts b/src/config/experience.ts
@@ -1,60 +0,0 @@
-export interface Experience {
- id: string;
- company: string;
- role: string;
- startDate: string;
- endDate?: string;
- description?: string[];
- tech?: string[];
- url?: string;
- logo?: string;
- logoFilter?: string;
- images?: string[];
-}
-
-export const experience = [
- {
- id: "stealth",
- company: "Stealth Startup",
- role: "SWE Intern",
- startDate: "2026-02",
- description: [],
- tech: [],
- },
- {
- id: "mandolin",
- company: "Mandolin",
- role: "Senior SWE Intern",
- startDate: "2024-10",
- endDate: "2025-02",
- description: [
- "Building AI agents for specialty drug access. Automating prior authorizations, benefits verification, and revenue cycle management.",
- "Mandolin raised $40M to automate access to life-saving medicines, wiring AI labor into thousands of infusion centers, specialty pharmacies, and health systems.",
- ],
- tech: ["TypeScript", "Temporal"],
- url: "https://mandolin.com",
- logo: "/mandolin/logo.svg",
- images: [
- "/mandolin/company.jpeg",
- "/mandolin/company_silly.jpeg",
- "/mandolin/employee_polaroids.jpeg",
- "/mandolin/moment_polaroids.jpeg",
- ],
- },
- {
- id: "amplifier-health",
- company: "Amplifier Health",
- role: "SWE Intern",
- startDate: "2024-06",
- endDate: "2024-08",
- description: [
- "Voice AI for early disease detection. Large Acoustic Model analyzing 1000+ voice biomarkers across 30+ health conditions.",
- ],
- tech: ["Python", "PyTorch"],
- url: "https://amplifierhealth.com",
- logo: "/amplifier/logo.svg",
- logoFilter: "logo-invert",
- },
-] as Experience[];
-
-export const currentJob = experience.find((exp) => !exp.endDate);
diff --git a/src/config/index.ts b/src/config/index.ts
@@ -1,4 +0,0 @@
-export * from "./about";
-export * from "./contact";
-export * from "./experience";
-export * from "./projects";
diff --git a/src/config/projects.ts b/src/config/projects.ts
@@ -1,44 +0,0 @@
-export default interface Project {
- id: string;
- name: string;
- subtitle: string;
- tech: string[];
- url: string;
- apiUrl: string;
-}
-
-export const projects = [
- {
- id: "janitor",
- name: "janitor",
- subtitle: "Audio classification with AST",
- tech: ["Rust", "CUDA"],
- url: "https://git.prime8.dev/moncheeta/janitor",
- apiUrl: "https://git.prime8.dev/api/v4/projects/moncheeta%2Fjanitor",
- },
- {
- id: "summarizer",
- name: "summarizer",
- subtitle: "Audio transcription microservices",
- tech: ["Python", "Docker", "Kafka"],
- url: "https://git.prime8.dev/moncheeta/summarizer",
- apiUrl: "https://git.prime8.dev/api/v4/projects/moncheeta%2Fsummarizer",
- },
- {
- id: "finger",
- name: "finger",
- subtitle: "Mobile auth app",
- tech: ["TypeScript", "React Native", "Supabase"],
- url: "https://git.prime8.dev/moncheeta/finger",
- apiUrl: "https://git.prime8.dev/api/v4/projects/moncheeta%2Ffinger",
- },
- {
- id: "computer-science-club",
- name: "computer-science-club",
- subtitle: "CS Club website",
- tech: ["Python", "REST"],
- url: "https://git.prime8.dev/moncheeta/computer-science-club",
- apiUrl:
- "https://git.prime8.dev/api/v4/projects/moncheeta%2Fcomputer-science-club",
- },
-] as Project[];
diff --git a/src/config/tabs.ts b/src/config/tabs.ts
@@ -1,8 +0,0 @@
-export const TABS = [
- { id: "about", label: "About" },
- { id: "experience", label: "Experience" },
- { id: "projects", label: "Projects" },
- { id: "blog", label: "Blog" },
-] as const;
-
-export type TabId = (typeof TABS)[number]["id"];
diff --git a/src/constants/calendar.ts b/src/constants/calendar.ts
@@ -1,24 +0,0 @@
-export const MONTH_NAMES = [
- "January",
- "February",
- "March",
- "April",
- "May",
- "June",
- "July",
- "August",
- "September",
- "October",
- "November",
- "December",
-];
-
-export const DAY_NAMES = [
- "Sunday",
- "Monday",
- "Tuesday",
- "Wednesday",
- "Thursday",
- "Friday",
- "Saturday",
-];
diff --git a/src/constants/env.ts b/src/constants/env.ts
@@ -1 +0,0 @@
-export default import.meta.env.NODE_ENV as "dev" | "prod";
diff --git a/src/constants/external/wakatime.ts b/src/constants/external/wakatime.ts
@@ -1,3 +0,0 @@
-export const WAKATIME_BASE_URL = "https://wakatime.com";
-export const WAKATIME_API_BASE = `${WAKATIME_BASE_URL}/api/v1`;
-export const WAKATIME_API_KEY = import.meta.env.WAKATIME_API_KEY;
diff --git a/src/constants/ui/boot.ts b/src/constants/ui/boot.ts
@@ -1,11 +0,0 @@
-import { about } from "../../config";
-import { SITE_DOMAIN } from "../website";
-
-export const TITLE = about.name;
-export const SUBTITLE = SITE_DOMAIN;
-
-export const GLITCH_CHARS = "▓▒░█▄▀■□▪▫●○◆◇";
-export const SCRAMBLE_DURATION = 400;
-export const SCRAMBLE_INTERVAL = 30;
-export const HOLD_DURATION = 200;
-export const FADE_DURATION = 300;
diff --git a/src/constants/ui/scroll.ts b/src/constants/ui/scroll.ts
@@ -1 +0,0 @@
-export const SCROLL_AMOUNT = 200;
diff --git a/src/constants/ui/tags.ts b/src/constants/ui/tags.ts
@@ -1,34 +0,0 @@
-export const TAG_CLASS_MAP: Record<string, string> = {
- // Languages
- Zig: "lang-zig",
- Rust: "lang-rust",
- C: "lang-c",
- Go: "lang-go",
- TypeScript: "lang-ts",
- Python: "lang-python",
- // Tools
- "Zed (Vim mode)": "tool-zed",
- Neovim: "tool-neovim",
- OpenCode: "tool-opencode",
- Linux: "tool-linux",
- Fish: "tool-fish",
- Ghostty: "tool-ghostty",
- // Interests
- "Low-level systems": "interest-lowlevel",
- "TUI/CLI tooling": "interest-tui",
- Embedded: "interest-embedded",
- "UNIX philosophy": "interest-unix",
- // Tech
- Temporal: "tech-temporal",
- PyTorch: "tech-pytorch",
- CUDA: "tech-cuda",
- Docker: "tech-docker",
- Kafka: "tech-kafka",
- "React Native": "tech-rn",
- Supabase: "tech-supabase",
- REST: "tech-rest",
-};
-
-export const TECH_ABBREVIATIONS: Record<string, string> = {
- TypeScript: "TS",
-};
diff --git a/src/constants/website.ts b/src/constants/website.ts
@@ -1,3 +0,0 @@
-export const SITE_DOMAIN = "www.prime8.dev";
-
-export const SITE_URL = `https://${SITE_DOMAIN}`;
diff --git a/src/external/git.ts b/src/external/git.ts
@@ -1,85 +0,0 @@
-import type Project from "../config/projects";
-
-export interface GitMetadata {
- readme: string | null;
- dateCreated: Date;
- dateModified: Date;
-}
-
-interface RepoUrls {
- web: string;
- branch: string;
-}
-
-export async function fetchGitMetadataForProject(
- project: Project,
-): Promise<GitMetadata | null> {
- try {
- const response = await fetch(getInternalGitLabUrl(project.apiUrl), {
- headers: { "User-Agent": "prime8dev-portfolio" },
- });
-
- if (!response.ok) return null;
-
- const data = await response.json();
-
- const urls: RepoUrls = {
- web: data.web_url.replace("http://", "https://"),
- branch: data.default_branch,
- };
-
- return {
- readme: await fetchReadme(
- urls,
- data.readme_url
- .replace("http://", "https://")
- .replace("/-/blob/", "/-/raw/"),
- ),
- dateCreated: data.created_at,
- dateModified: data.updated_at,
- };
- } catch {
- return null;
- }
-}
-
-function getInternalGitLabUrl(externalUrl: string): string {
- const internalHost = process.env.GITLAB_HOST;
- if (!internalHost) return externalUrl;
- return externalUrl.replace(/https?:\/\/[^/]+/, internalHost);
-}
-
-async function fetchReadme(
- urls: RepoUrls,
- readmeUrl: string,
-): Promise<string | null> {
- try {
- const response = await fetch(getInternalGitLabUrl(readmeUrl), {
- headers: { "User-Agent": "prime8dev-portfolio" },
- });
- if (!response.ok) return null;
-
- const content = await response.text();
- return fixRelativePaths(urls, content);
- } catch {
- return null;
- }
-}
-
-function fixRelativePaths(urls: RepoUrls, markdown: string): string {
- // blob = file browser URL (for links)
- const blob = `${urls.web}/-/blob/${urls.branch}`;
- markdown = markdown.replace(
- /(?<!!)\[([^\]]+)\]\((?!https?:\/\/)([^)]+)\)/g,
- `[$1](${blob}/$2)`,
- );
-
- // raw = direct file content URL (for images)
- const raw = `${urls.web}/-/raw/${urls.branch}`;
- markdown = markdown.replace(
- /!\[([^\]]*)\]\((?!https?:\/\/)([^)]+)\)/g,
- ``,
- );
-
- return markdown;
-}
diff --git a/src/external/wakatime.ts b/src/external/wakatime.ts
@@ -1,159 +0,0 @@
-import {
- WAKATIME_API_BASE,
- WAKATIME_API_KEY,
-} from "../constants/external/wakatime";
-import { DAY_NAMES, MONTH_NAMES } from "../constants/calendar";
-import { formatDuration } from "../utils/duration";
-
-export interface WakaTimeData {
- allTimeStats: AllTimeStats | null;
- weeklyStats: WeeklyStats | null;
-}
-
-export async function fetchWakaTimeData(): Promise<WakaTimeData | null> {
- if (!WAKATIME_API_KEY) {
- return null;
- }
-
- try {
- const [allTimeStats, weeklyStats] = await Promise.all([
- fetchAllTimeStats(),
- fetchWeeklyStats(),
- ]);
-
- return { allTimeStats, weeklyStats };
- } catch (error) {
- console.error("Failed to fetch WakaTime data:", error);
- return null;
- }
-}
-
-export interface AllTimeStats {
- totalText: string;
- dailyAverageText: string;
-}
-
-export async function fetchAllTimeStats(): Promise<AllTimeStats | null> {
- try {
- const [statsResponse, languagesResponse] = [
- await fetch(`${WAKATIME_API_BASE}/users/current/all_time_since_today`, {
- headers: {
- Authorization: `Basic ${Buffer.from(WAKATIME_API_KEY).toString("base64")}`,
- },
- }),
- await fetch(`${WAKATIME_API_BASE}/users/current/stats/last_year`, {
- headers: {
- Authorization: `Basic ${Buffer.from(WAKATIME_API_KEY).toString("base64")}`,
- },
- }),
- ];
- if ([statsResponse, languagesResponse].some((r) => !r.ok)) {
- return null;
- }
-
- const [statsData, langData] = await Promise.all(
- [statsResponse, languagesResponse].map((r) => r.json()),
- );
-
- let dailyAverageText = "1 hr";
- if (langData.data?.daily_average_including_other_language) {
- const avgSeconds = langData.data.daily_average_including_other_language;
- dailyAverageText = formatDuration(avgSeconds);
- }
-
- const totalSeconds = statsData.data?.total_seconds || 0;
- const totalHours = Math.floor(totalSeconds / 3600);
- const totalText = `${totalHours.toLocaleString()}+ hrs coded`;
-
- return {
- totalText,
- dailyAverageText,
- };
- } catch (error) {
- console.error("Failed to fetch WakaTime stats:", error);
- return null;
- }
-}
-
-export interface WeeklyStats {
- totalText: string;
- days: DailyEntry[];
-}
-
-export interface DailyEntry {
- dayText: string; // e.g., "Mon", "Tue"
- dateText: string; // e.g., "Jan 10"
- timeText: string; // e.g., "2 hrs 30 mins"
- barWidthPercent: string; // e.g., "75%"
-}
-
-export async function fetchWeeklyStats(): Promise<WeeklyStats | null> {
- try {
- const start = new Date();
- start.setDate(start.getDate() - 6); // 7 days including today
- const end = new Date();
-
- const formatDate = (d: Date) => d.toISOString().split("T")[0];
-
- const response = await fetch(
- `${WAKATIME_API_BASE}/users/current/summaries?start=${formatDate(start)}&end=${formatDate(end)}`,
- {
- headers: {
- Authorization: `Basic ${Buffer.from(WAKATIME_API_KEY).toString("base64")}`,
- },
- },
- );
-
- if (!response.ok) {
- console.error("WakaTime weekly stats error:", response.status);
- return null;
- }
-
- const { data } = await response.json();
-
- let totalSeconds = 0;
- let maxMinutes = 0;
-
- const minutesPerDay: number[] = [];
- const days: DailyEntry[] = (data || []).map(
- (
- {
- range,
- grand_total,
- }: {
- range: { date: string };
- grand_total: { total_seconds?: number };
- },
- i: number,
- ) => {
- const date = new Date(range.date);
- const dayText = DAY_NAMES[date.getUTCDay()].slice(0, 3);
- const dateText = `${MONTH_NAMES[date.getUTCMonth()].slice(0, 3)} ${date.getUTCDate()}`;
-
- const seconds = grand_total?.total_seconds || 0;
- totalSeconds += seconds;
-
- const totalMinutes = Math.floor(seconds / 60);
- minutesPerDay[i] = totalMinutes;
- if (totalMinutes > maxMinutes) maxMinutes = totalMinutes;
-
- const timeText = formatDuration(seconds);
-
- return { dayText, dateText, timeText, barWidthPercent: "0%" };
- },
- );
-
- const effectiveMax = maxMinutes || 1;
- for (let i = 0; i < days.length; i++) {
- days[i].barWidthPercent =
- `${Math.round((minutesPerDay[i] / effectiveMax) * 100)}%`;
- }
-
- const totalText = formatDuration(totalSeconds);
-
- return { totalText, days };
- } catch (error) {
- console.error("Failed to fetch weekly stats:", error);
- return null;
- }
-}
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
@@ -1,547 +0,0 @@
----
-import Lightbox from "./components/Lightbox.astro";
-
-import { SITE_URL } from "../constants/website";
-import { about } from "../config";
-
-interface Props {
- title: string;
- description: string;
- type: "website" | "article";
- url: string;
- image?: string;
- keywords?: string;
- publishedTime?: string;
- modifiedTime?: string;
-}
-
-const {
- title,
- description,
- type,
- url,
- image,
- keywords,
- publishedTime,
- modifiedTime,
-} = Astro.props;
-
-const ogImage = image
- ? image.startsWith("http")
- ? image
- : `${SITE_URL}${image}`
- : `${SITE_URL}/me.jpg`;
----
-
-<!doctype html>
-<html lang="en">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>{title}</title>
- <meta name="description" content={description} />
- {keywords && <meta name="keywords" content={keywords} />}
-
- <link rel="canonical" href={url} />
- <link rel="alternate" hreflang="en" href={url} />
- <link rel="alternate" hreflang="x-default" href={url} />
-
- <!-- Preconnect for external resources -->
- <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
- <link rel="dns-prefetch" href="https://cdn.jsdelivr.net" />
-
- <!-- Preload font for performance -->
- <link
- rel="preload"
- href="https://cdn.jsdelivr.net/npm/@fontsource/jetbrains-mono@5.0.18/files/jetbrains-mono-latin-400-normal.woff2"
- as="font"
- type="font/woff2"
- crossorigin
- />
-
- <!-- Open Graph -->
- <meta property="og:type" content={type} />
- <meta property="og:url" content={url} />
- <meta property="og:title" content={title} />
- <meta property="og:description" content={description} />
- <meta property="og:image" content={ogImage} />
- <meta property="og:image:width" content="1200" />
- <meta property="og:image:height" content="630" />
- <meta property="og:site_name" content={about.name} />
- <meta property="og:locale" content="en_US" />
- {
- type === "article" && publishedTime && (
- <meta property="article:published_time" content={publishedTime} />
- )
- }
- {
- type === "article" && modifiedTime && (
- <meta property="article:modified_time" content={modifiedTime} />
- )
- }
- {
- type === "article" && (
- <meta property="article:author" content={about.name} />
- )
- }
-
- <!-- Twitter Card -->
- <meta name="twitter:card" content="summary_large_image" />
- <meta name="twitter:url" content={url} />
- <meta name="twitter:title" content={title} />
- <meta name="twitter:description" content={description} />
- <meta name="twitter:image" content={ogImage} />
-
- <!-- Additional Meta -->
- <meta name="author" content={about.name} />
- <meta name="robots" content="index, follow" />
- <meta name="theme-color" content="#0d0d0d" />
-
- <!-- Favicons -->
- <link rel="icon" type="image/png" href="/favicon.png" />
- <link rel="apple-touch-icon" href="/favicon.png" />
- <link rel="manifest" href="/site.webmanifest" />
- </head>
- <body>
- <div id="app">
- <slot />
- </div>
-
- <Lightbox />
- </body>
-</html>
-
-<style is:global>
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
-
- :root {
- --bg: #0d0d0d;
- --bg-alt: #161616;
- --fg: #c0c0c0;
- --fg-dim: #606060;
- --accent: #d7af5f;
- --accent-dim: #a68a3d;
- --cyan: #5fafaf;
- --blue: #5f87d7;
- --light-blue: #5fafff;
- --yellow: #d7af5f;
- --magenta: #af5faf;
- --red: #d75f5f;
- --border: #333;
- --selection: #3d3520;
- }
-
- @font-face {
- font-family: "JetBrains Mono";
- src: url("https://cdn.jsdelivr.net/npm/@fontsource/jetbrains-mono@5.0.18/files/jetbrains-mono-latin-400-normal.woff2")
- format("woff2");
- font-weight: 400;
- }
-
- body {
- background: var(--bg);
- color: var(--fg);
- font-family: "JetBrains Mono", "SF Mono", "Fira Code", monospace;
- font-size: 14px;
- line-height: 1.5;
- min-height: 100vh;
- overflow: hidden;
- }
-
- ::-webkit-scrollbar {
- width: 8px;
- }
-
- ::-webkit-scrollbar-track {
- background: var(--bg);
- }
-
- ::-webkit-scrollbar-thumb {
- background: var(--fg-dim);
- border-radius: 4px;
- }
-
- ::-webkit-scrollbar-thumb:hover {
- background: var(--fg);
- }
-
- .link {
- color: var(--light-blue);
- text-decoration: underline;
- cursor: pointer;
- }
-
- .link:hover {
- color: var(--light-blue);
- opacity: 0.8;
- }
-
- a {
- color: var(--light-blue);
- text-decoration: none;
- }
-
- a:hover {
- opacity: 0.8;
- }
-
- .color-green {
- color: var(--accent);
- }
- .color-blue {
- color: var(--blue);
- }
- .color-cyan {
- color: var(--cyan);
- }
- .color-yellow {
- color: var(--yellow);
- }
- .color-red {
- color: var(--red);
- }
- .color-magenta {
- color: var(--magenta);
- }
- .color-dim {
- color: var(--fg-dim);
- }
- .bold {
- font-weight: 700;
- }
-
- #app {
- display: flex;
- flex-direction: column;
- height: 100vh;
- opacity: 0;
- }
-
- #app.ready {
- opacity: 1;
- }
-
- #content {
- flex: 1;
- overflow: hidden;
- }
-
- .page {
- display: none;
- height: 100%;
- }
-
- .page.active {
- display: flex;
- }
-
- .tag {
- border: none;
- padding: 2px 8px;
- font-size: 12px;
- color: var(--bg);
- background: var(--fg-dim);
- }
-
- .tag.lang-zig {
- background: #f7a41d;
- }
- .tag.lang-rust {
- background: #dea584;
- }
- .tag.lang-c {
- background: #a8b1c2;
- }
- .tag.lang-go {
- background: #00add8;
- }
- .tag.lang-ts {
- background: #3178c6;
- color: #fff;
- }
- .tag.lang-python {
- background: #3572a5;
- color: #fff;
- }
-
- .tag.tool-zed {
- background: #084ccf;
- color: #fff;
- }
- .tag.tool-neovim {
- background: #57a143;
- }
- .tag.tool-opencode {
- background: #af5faf;
- color: #fff;
- }
- .tag.tool-linux {
- background: #f7c94c;
- }
- .tag.tool-fish {
- background: #d78700;
- }
- .tag.tool-ghostty {
- background: #8b5cf6;
- color: #fff;
- }
-
- .tag.interest-lowlevel {
- background: #d75f5f;
- color: #fff;
- }
- .tag.interest-tui {
- background: #5faf5f;
- color: #0d0d0d;
- }
- .tag.interest-embedded {
- background: #5fafaf;
- color: #0d0d0d;
- }
- .tag.interest-unix {
- background: #d7af5f;
- color: #0d0d0d;
- }
-
- .tag.tech-temporal {
- background: #4c35c9;
- color: #fff;
- }
- .tag.tech-postgres {
- background: #336791;
- color: #fff;
- }
- .tag.tech-pytorch {
- background: #ee4c2c;
- color: #fff;
- }
- .tag.tech-cuda {
- background: #76b900;
- }
- .tag.tech-docker {
- background: #2496ed;
- color: #fff;
- }
- .tag.tech-kafka {
- background: #4c35c9;
- color: #fff;
- }
- .tag.tech-rn {
- background: #61dafb;
- }
- .tag.tech-supabase {
- background: #3ecf8e;
- }
- .tag.tech-rest {
- background: #6b8e23;
- }
-
- @media (max-width: 768px) {
- body {
- font-size: 13px;
- overflow-y: auto;
- }
-
- #app {
- display: flex;
- flex-direction: column;
- min-height: 100vh;
- height: auto;
- }
-
- #content {
- flex: 1;
- overflow: visible;
- }
-
- .page {
- height: auto;
- min-height: 100vh;
- }
-
- .page.active {
- display: block;
- }
- }
-
- /* Boot Animation */
- #boot-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100vw;
- height: 100vh;
- background: var(--bg);
- z-index: 9999;
- display: flex;
- align-items: center;
- justify-content: center;
- overflow: hidden;
- }
-
- #boot-overlay.fade-out {
- animation: bootFadeOut 0.3s ease-out forwards;
- }
-
- .boot-terminal {
- text-align: center;
- }
-
- .boot-text {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
-
- .boot-title {
- font-family: "JetBrains Mono", "SF Mono", monospace;
- font-size: 32px;
- font-weight: 600;
- color: var(--accent);
- letter-spacing: -0.5px;
- }
-
- .boot-subtitle {
- font-family: "JetBrains Mono", "SF Mono", monospace;
- font-size: 14px;
- color: var(--fg-dim);
- letter-spacing: 0.5px;
- }
-
- @keyframes bootFadeOut {
- 0% {
- opacity: 1;
- }
- 100% {
- opacity: 0;
- }
- }
-
- #app.boot-reveal {
- animation: revealContent 0.5s ease-out;
- }
-
- @keyframes revealContent {
- 0% {
- opacity: 0;
- transform: translateY(15px);
- }
- 100% {
- opacity: 1;
- transform: translateY(0);
- }
- }
-
- /* Help Overlay */
- #help-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100vw;
- height: 100vh;
- background: rgba(0, 0, 0, 0.8);
- z-index: 9998;
- display: flex;
- align-items: center;
- justify-content: center;
- opacity: 0;
- visibility: hidden;
- transition:
- opacity 0.2s,
- visibility 0.2s;
- }
-
- #help-overlay.visible {
- opacity: 1;
- visibility: visible;
- }
-
- .help-modal {
- background: var(--bg);
- border: 1px solid var(--border);
- padding: 20px 24px;
- min-width: 280px;
- max-width: 90vw;
- }
-
- .help-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 16px;
- padding-bottom: 12px;
- border-bottom: 1px solid var(--border);
- }
-
- .help-title {
- color: var(--accent);
- font-weight: 600;
- }
-
- .help-close {
- color: var(--fg-dim);
- font-size: 11px;
- }
-
- .help-content {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
-
- .help-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 24px;
- }
-
- .help-key {
- color: var(--cyan);
- font-family: "JetBrains Mono", monospace;
- font-size: 12px;
- min-width: 60px;
- }
-
- .help-desc {
- color: var(--fg);
- font-size: 13px;
- }
-
- @media (max-width: 768px) {
- .help-modal {
- padding: 16px 20px;
- min-width: 260px;
- }
-
- .help-header {
- margin-bottom: 12px;
- padding-bottom: 10px;
- }
-
- .help-title {
- font-size: 14px;
- }
-
- .help-close {
- font-size: 10px;
- }
-
- .help-row {
- gap: 16px;
- }
-
- .help-key {
- font-size: 11px;
- min-width: 50px;
- }
-
- .help-desc {
- font-size: 12px;
- }
- }
-</style>
diff --git a/src/layouts/components/Article.astro b/src/layouts/components/Article.astro
@@ -1,387 +0,0 @@
----
-import type { Blog } from "../../blogs";
-
-interface Props {
- blog: Blog;
-}
-
-const { blog } = Astro.props;
----
-
-<article class="blog-article" data-id={blog.id} data-blog-id={blog.id}>
- <header class="blog-article-header">
- <div class="blog-article-header-top">
- <h1 class="blog-article-title">{blog.title}</h1>
- <button
- class="blog-share-btn blog-share-btn-desktop"
- data-blog-id={blog.id}
- data-blog-title={blog.title}
- >
- [Share]
- </button>
- </div>
- <div class="blog-article-meta">
- <time class="blog-article-date" datetime={blog.date}>{blog.date}</time>
- <button
- class="blog-share-btn blog-share-btn-mobile"
- data-blog-id={blog.id}
- data-blog-title={blog.title}
- >
- [Share]
- </button>
- </div>
- </header>
- <div class="blog-article-content" data-blog-id={blog.id}>
- {
- blog.url ? (
- <a
- class="external-link"
- href={blog.url}
- target="_blank"
- rel="noopener noreferrer"
- >
- Read on external site →
- </a>
- ) : blog.content ? (
- <Fragment set:html={blog.content} />
- ) : (
- <p class="color-dim">Content not available</p>
- )
- }
- </div>
-</article>
-
-<style>
- .blog-article {
- animation: fadeIn 0.2s ease;
- }
-
- @keyframes fadeIn {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
- }
-
- .blog-article-header {
- margin-bottom: 24px;
- padding-bottom: 16px;
- border-bottom: 1px solid var(--border);
- }
-
- .blog-article-header-top {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 16px;
- }
-
- .blog-article-title {
- font-size: 28px;
- font-weight: 500;
- color: var(--accent);
- margin: 0 0 8px 0;
- line-height: 1.3;
- }
-
- .blog-share-btn {
- background: transparent;
- border: 1px solid var(--border);
- color: var(--fg-dim);
- padding: 6px 12px;
- cursor: pointer;
- font-family: inherit;
- font-size: 12px;
- transition: all 0.15s;
- flex-shrink: 0;
- }
-
- .blog-share-btn:hover {
- background: var(--bg-alt);
- color: var(--accent);
- border-color: var(--accent);
- }
-
- .blog-share-btn.copied {
- color: var(--green);
- border-color: var(--green);
- }
-
- .blog-article-meta {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
-
- .blog-article-date {
- color: var(--fg-dim);
- font-size: 13px;
- }
-
- .blog-share-btn-mobile {
- display: none;
- }
-
- .blog-article-content {
- font-size: 14px;
- line-height: 1.7;
- color: var(--fg);
- padding-bottom: 32px;
- }
-
- .blog-article-content :global(p) {
- margin-bottom: 16px;
- }
-
- .blog-article-content :global(p:last-child) {
- margin-bottom: 0;
- }
-
- .blog-article-content :global(h2) {
- font-size: 20px;
- color: var(--accent);
- margin: 32px 0 12px;
- font-weight: 600;
- }
-
- .blog-article-content :global(h2:first-child) {
- margin-top: 0;
- }
-
- .blog-article-content :global(h3) {
- font-size: 17px;
- color: var(--accent);
- margin: 24px 0 10px;
- font-weight: 500;
- }
-
- .blog-article-content :global(h4) {
- font-size: 15px;
- color: var(--fg);
- margin: 20px 0 8px;
- font-weight: 500;
- }
-
- .blog-article-content :global(h5),
- .blog-article-content :global(h6) {
- font-size: 14px;
- color: var(--fg-dim);
- margin: 16px 0 8px;
- font-weight: 500;
- }
-
- .blog-article-content :global(ul),
- .blog-article-content :global(ol) {
- margin: 12px 0;
- padding-left: 24px;
- }
-
- .blog-article-content :global(li) {
- margin-bottom: 8px;
- }
-
- .blog-article-content :global(li > ul),
- .blog-article-content :global(li > ol) {
- margin: 6px 0;
- }
-
- .blog-article-content :global(code) {
- background: var(--bg-alt);
- padding: 2px 6px;
- font-size: 13px;
- border-radius: 2px;
- }
-
- .blog-article-content :global(pre) {
- display: block;
- background: var(--bg-alt);
- border: 1px solid var(--border);
- padding: 16px;
- margin: 16px 0;
- overflow-x: auto;
- font-size: 13px;
- line-height: 1.5;
- }
-
- .blog-article-content :global(pre code) {
- background: none;
- padding: 0;
- font-size: inherit;
- }
-
- .blog-article-content :global(a) {
- color: var(--light-blue);
- text-decoration: none;
- }
-
- .blog-article-content :global(a:hover) {
- opacity: 0.8;
- }
-
- .blog-article-content :global(strong) {
- font-weight: 600;
- color: var(--fg);
- }
-
- .blog-article-content :global(em) {
- font-style: italic;
- color: var(--fg-dim);
- }
-
- .blog-article-content :global(del) {
- text-decoration: line-through;
- color: var(--fg-dim);
- }
-
- .blog-article-content :global(blockquote) {
- border-left: 3px solid var(--accent);
- margin: 16px 0;
- padding: 12px 20px;
- color: var(--fg-dim);
- background: var(--bg-alt);
- }
-
- .blog-article-content :global(blockquote p) {
- margin-bottom: 0;
- }
-
- .blog-article-content :global(hr) {
- border: none;
- border-top: 1px solid var(--border);
- margin: 24px 0;
- }
-
- .blog-article-content :global(table) {
- width: 100%;
- border-collapse: collapse;
- margin: 16px 0;
- font-size: 13px;
- }
-
- .blog-article-content :global(th),
- .blog-article-content :global(td) {
- border: 1px solid var(--border);
- padding: 10px 14px;
- text-align: left;
- }
-
- .blog-article-content :global(th) {
- background: var(--bg-alt);
- color: var(--accent);
- font-weight: 500;
- }
-
- .blog-article-content :global(tr:nth-child(even)) {
- background: rgba(255, 255, 255, 0.02);
- }
-
- .blog-article-content :global(img) {
- max-width: 100%;
- height: auto;
- margin: 16px 0;
- }
-
- .external-link {
- color: var(--light-blue);
- text-decoration: none;
- font-size: 14px;
- }
-
- .external-link:hover {
- opacity: 0.8;
- }
-
- @media (max-width: 768px) {
- .blog-article-header {
- margin-bottom: 16px;
- padding-bottom: 12px;
- }
-
- .blog-article-title {
- font-size: 22px;
- }
-
- .blog-share-btn-desktop {
- display: none;
- }
-
- .blog-share-btn-mobile {
- display: block;
- }
-
- .blog-article-content {
- font-size: 13px;
- }
-
- .blog-article-content :global(h2) {
- font-size: 18px;
- margin: 24px 0 10px;
- }
-
- .blog-article-content :global(h3) {
- font-size: 15px;
- margin: 20px 0 8px;
- }
-
- .blog-article-content :global(pre) {
- padding: 12px;
- font-size: 12px;
- }
-
- .blog-article-content :global(code) {
- font-size: 12px;
- }
-
- .blog-article-content :global(table) {
- font-size: 12px;
- }
-
- .blog-article-content :global(th),
- .blog-article-content :global(td) {
- padding: 8px 10px;
- }
- }
-</style>
-
-<script>
- document.addEventListener("DOMContentLoaded", () => {
- const shareButtons = document.querySelectorAll(".blog-share-btn");
-
- shareButtons.forEach((btn) => {
- btn.addEventListener("click", async (e) => {
- e.stopPropagation();
- const button = e.currentTarget as HTMLButtonElement;
- const blogId = button.dataset.blogId;
- const title = button.dataset.blogTitle;
- const url = `${window.location.origin}/blog/${blogId}`;
-
- if (navigator.share) {
- try {
- await navigator.share({
- title,
- url,
- });
- } catch {
- // User cancelled share dialog
- }
- } else {
- copyToClipboard(url, button);
- }
- });
- });
-
- function copyToClipboard(text: string, btn: HTMLButtonElement) {
- navigator.clipboard.writeText(text).then(() => {
- const originalText = btn.textContent;
- btn.textContent = "[Copied!]";
- btn.classList.add("copied");
- setTimeout(() => {
- btn.textContent = originalText;
- btn.classList.remove("copied");
- }, 2000);
- });
- }
- });
-</script>
diff --git a/src/layouts/components/Footer.astro b/src/layouts/components/Footer.astro
@@ -1,60 +0,0 @@
----
-import { WAKATIME_BASE_URL } from "../../constants/external/wakatime";
-import { about } from "../../config";
-import type { WakaTimeData } from "../../external/wakatime";
-
-interface Props {
- wakaTimeData: WakaTimeData | null;
-}
-
-const { wakaTimeData } = Astro.props;
----
-
-<footer id="statusbar">
- <span class="time" id="status-time"></span>
- <span class="spacer"></span>
- {
- wakaTimeData?.weeklyStats && (
- <a
- class="wakatime"
- href={`${WAKATIME_BASE_URL}/@${about.handle}`}
- target="_blank"
- rel="noopener noreferrer"
- >
- {wakaTimeData.allTimeStats?.totalText}
- </a>
- )
- }
-</footer>
-
-<style>
- #statusbar {
- display: flex;
- align-items: center;
- background: var(--bg-alt);
- border-top: 1px solid var(--border);
- padding: 0 12px;
- height: 24px;
- font-size: 12px;
- position: sticky;
- bottom: 0;
- z-index: 100;
- }
-
- #statusbar .time {
- color: var(--cyan);
- }
-
- #statusbar .spacer {
- flex: 1;
- }
-
- #statusbar .wakatime {
- color: var(--magenta);
- text-decoration: none;
- }
-
- #statusbar .wakatime:hover {
- opacity: 0.8;
- }
-</style>
diff --git a/src/layouts/components/Header.astro b/src/layouts/components/Header.astro
@@ -1,156 +0,0 @@
----
-import { TABS } from "../../config/tabs";
----
-
-<header id="tabs">
- <nav class="tab-nav" aria-label="Main navigation">
- {
- TABS.map((tab, i) => (
- <div
- class:list={["tab", { active: i === 0 }]}
- data-tab={tab.id}
- aria-current={i === 0 ? "page" : undefined}
- >
- <span class="tab-num">[{i + 1}] </span>
- {tab.label}
- </div>
- ))
- }
- </nav>
- <div class="tab-spacer"></div>
- <div class="status" id="header-status">
- <span class="dpad">
- <span class="dpad-row"><span class="dpad-key">↑</span></span>
- <span class="dpad-row"
- ><span class="dpad-key">←</span><span class="dpad-key">↓</span><span
- class="dpad-key">→</span
- ></span
- >
- </span>
- <span class="navigate"> navigate · Tab cycle · ? help</span>
- </div>
-</header>
-
-<style>
- #tabs {
- display: flex;
- align-items: center;
- gap: 4px;
- background: var(--bg-alt);
- border-bottom: 1px solid var(--border);
- padding: 0 12px;
- height: 36px;
- position: sticky;
- top: 0;
- z-index: 100;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- }
-
- #tabs * {
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- }
-
- .tab-nav {
- display: flex;
- align-items: center;
- gap: 4px;
- }
-
- .tab {
- padding: 6px 12px;
- color: var(--fg-dim);
- cursor: pointer;
- border-bottom: 2px solid transparent;
- transition:
- color 0.15s,
- border-color 0.15s;
- }
-
- .tab:hover {
- color: var(--fg);
- }
-
- .tab.active {
- color: var(--accent);
- border-bottom-color: var(--accent);
- }
-
- .tab-spacer {
- flex: 1;
- }
-
- .status {
- display: flex;
- align-items: center;
- gap: 8px;
- color: var(--fg-dim);
- font-size: 11px;
- margin-right: 8px;
- }
-
- .dpad {
- display: inline-flex;
- flex-direction: column;
- align-items: center;
- gap: 1px;
- font-size: 8px;
- line-height: 1;
- }
-
- .dpad-row {
- display: flex;
- gap: 1px;
- }
-
- .dpad-key {
- width: 12px;
- height: 10px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: var(--border);
- border-radius: 2px;
- color: var(--fg);
- }
-
- @media (max-width: 768px) {
- #tabs {
- flex-wrap: wrap;
- height: auto;
- min-height: 36px;
- padding: 8px 12px;
- gap: 8px;
- }
-
- .tab-nav {
- width: 100%;
- justify-content: space-between;
- gap: 0;
- }
-
- .tab {
- padding: 6px 8px;
- font-size: 12px;
- flex: 1;
- text-align: center;
- }
-
- .tab-spacer {
- display: none;
- }
-
- .status {
- display: none;
- }
-
- .tab-num {
- display: none;
- }
- }
-</style>
diff --git a/src/layouts/components/Lightbox.astro b/src/layouts/components/Lightbox.astro
@@ -1,135 +0,0 @@
-<div id="image-lightbox" class="lightbox hidden">
- <div class="lightbox-overlay"></div>
- <div class="lightbox-content">
- <img src="" alt="" class="lightbox-image" />
- <button class="lightbox-close">[X]</button>
- </div>
-</div>
-
-<style>
- .lightbox {
- position: fixed;
- inset: 0;
- z-index: 1000;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .lightbox.hidden {
- display: none;
- }
-
- .lightbox-overlay {
- position: absolute;
- inset: 0;
- background: rgba(0, 0, 0, 0.9);
- cursor: pointer;
- }
-
- .lightbox-content {
- position: relative;
- max-width: 90vw;
- max-height: 90vh;
- z-index: 1;
- }
-
- .lightbox-image {
- max-width: 100%;
- max-height: 90vh;
- border-radius: 4px;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
- }
-
- .lightbox-close {
- position: absolute;
- top: 8px;
- right: 8px;
- background: rgba(0, 0, 0, 0.7);
- border: 1px solid var(--border);
- color: var(--fg);
- font-family: "JetBrains Mono", monospace;
- font-size: 12px;
- cursor: pointer;
- padding: 4px 8px;
- line-height: 1;
- transition:
- color 0.15s,
- background 0.15s;
- z-index: 10;
- }
-
- .lightbox-close:hover {
- color: var(--accent);
- background: rgba(0, 0, 0, 0.9);
- }
-</style>
-
-<script>
- document.addEventListener("DOMContentLoaded", () => {
- const lightbox = document.getElementById("image-lightbox");
- const lightboxImage = lightbox?.querySelector(
- ".lightbox-image",
- ) as HTMLImageElement;
- const lightboxOverlay = lightbox?.querySelector(".lightbox-overlay");
- const lightboxClose = lightbox?.querySelector(".lightbox-close");
-
- let lightboxImages: string[] = [];
- let currentLightboxIndex = 0;
-
- (window as Window & { isLightboxOpen?: () => boolean }).isLightboxOpen =
- () => !lightbox?.classList.contains("hidden");
-
- (
- window as Window & {
- openLightbox?: (images: string[], startIndex?: number) => void;
- }
- ).openLightbox = (images: string[], startIndex: number = 0) => {
- if (lightbox && lightboxImage && images.length > 0) {
- lightboxImages = images;
- currentLightboxIndex = startIndex;
- lightboxImage.src = images[startIndex];
- lightbox.classList.remove("hidden");
- }
- };
-
- function closeLightbox() {
- lightbox?.classList.add("hidden");
- lightboxImages = [];
- currentLightboxIndex = 0;
- }
-
- function navigateLightboxImage(direction: number) {
- if (lightboxImages.length <= 1) return;
- currentLightboxIndex =
- (currentLightboxIndex + direction + lightboxImages.length) %
- lightboxImages.length;
- if (lightboxImage) {
- lightboxImage.src = lightboxImages[currentLightboxIndex];
- }
- }
-
- (
- window as Window & { navigateLightboxImage?: (direction: number) => void }
- ).navigateLightboxImage = navigateLightboxImage;
-
- lightboxOverlay?.addEventListener("click", closeLightbox);
- lightboxClose?.addEventListener("click", closeLightbox);
-
- document.addEventListener("keydown", (e) => {
- if (!lightbox?.classList.contains("hidden")) {
- if (e.key === "Escape") {
- closeLightbox();
- } else if (e.key === "ArrowLeft") {
- e.preventDefault();
- e.stopPropagation();
- navigateLightboxImage(-1);
- } else if (e.key === "ArrowRight") {
- e.preventDefault();
- e.stopPropagation();
- navigateLightboxImage(1);
- }
- }
- });
- });
-</script>
diff --git a/src/layouts/tabs/About.astro b/src/layouts/tabs/About.astro
@@ -1,430 +0,0 @@
----
-import type { WakaTimeData } from "../../external/wakatime";
-import { about, contact } from "../../config";
-import { currentJob } from "../../config/experience";
-import { getTagClass } from "../../utils/tags";
-
-interface Props {
- wakaTimeData: WakaTimeData | null;
-}
-
-const { wakaTimeData } = Astro.props;
----
-
-<section id="about" class="page active">
- <div class="about-layout">
- <div class="about-left">
- <img
- class="profile-image"
- src="/me.jpg"
- alt="Damian Myrda - Software Engineer and high school senior from Chicago"
- loading="eager"
- />
- <div class="contact-box">
- <div class="box-title">─ Contact ─</div>
- <div class="contact-item">
- <span class="label">Email</span>
- <a href={`mailto:${contact.email}`}>{contact.email}</a>
- </div>
- <div class="contact-item">
- <span class="label">GitLab</span>
- <a href={contact.gitlab} target="_blank" rel="noopener noreferrer me"
- >git.prime8.dev/moncheeta</a
- >
- </div>
- <div class="contact-item">
- <span class="label">LinkedIn</span>
- <a
- href={contact.linkedin}
- target="_blank"
- rel="noopener noreferrer me">linkedin.com/in/damian-myrda</a
- >
- </div>
- </div>
- </div>
- <div class="about-right">
- <div class="section">
- <h1>
- {about.name}
- <span class="dim">@{about.handle}</span>
- </h1>
- <p class="bio">{about.bio}</p>
- </div>
- <div class="section">
- <div class="box-title">─ Info ─</div>
- <div class="info-columns">
- <div class="info-column">
- <div class="info-row">
- <span class="label">Location</span>
- {about.location}
- </div>
- <div class="info-row">
- <span class="label">School</span>
- {about.school}
- </div>
- <div class="info-row">
- <span class="label">Graduating</span>
- {about.graduating}
- </div>
- </div>
- {
- currentJob && (
- <div class="info-column">
- <div class="info-row">
- <span class="label">Company</span>
- <a href={`#experience/${currentJob.id}`}>
- {currentJob.company}
- </a>
- </div>
- <div class="info-row">
- <span class="label">Position</span>
- {currentJob.role}
- </div>
- </div>
- )
- }
- </div>
- </div>
- <div class="section">
- <div class="box-title">─ Interests ─</div>
- <div class="tags">
- {
- about.interests.map((interest) => (
- <span class={`tag ${getTagClass(interest)}`}>{interest}</span>
- ))
- }
- </div>
- </div>
- <div class="section">
- <div class="box-title">─ Tools ─</div>
- <div class="tags">
- {
- about.tools.map((tool) => (
- <span class={`tag ${getTagClass(tool)}`}>{tool}</span>
- ))
- }
- </div>
- </div>
- <div class="section">
- <div class="box-title">─ Languages ─</div>
- <div class="tags">
- {
- about.languages.map((lang) => (
- <span class={`tag ${getTagClass(lang)}`}>{lang}</span>
- ))
- }
- </div>
- </div>
- {
- wakaTimeData?.weeklyStats && (
- <div class="section">
- <div class="box-title">─ Activity ─</div>
- <div class="weekly-stats">
- <div class="weekly-total">
- {wakaTimeData.weeklyStats.totalText}
- </div>
- <div class="weekly-days">
- {wakaTimeData.weeklyStats.days.map((day) => (
- <div class="weekly-day-row">
- <span class="weekly-day-name">{day.dayText}</span>
- <div class="weekly-day-bar-container">
- <div
- class="weekly-day-bar"
- style={`width: ${day.barWidthPercent}`}
- />
- </div>
- <span class="weekly-day-time">{day.timeText}</span>
- </div>
- ))}
- </div>
- </div>
- </div>
- )
- }
- </div>
- </div>
-</section>
-
-<style>
- .about-layout {
- display: flex;
- width: 100%;
- height: 100%;
- }
-
- .about-left {
- width: 320px;
- flex-shrink: 0;
- border-right: 1px solid var(--border);
- display: flex;
- flex-direction: column;
- padding: 16px;
- gap: 16px;
- overflow-y: auto;
- }
-
- .about-right {
- flex: 1;
- padding: 24px 32px;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- justify-content: space-evenly;
- }
-
- .profile-image {
- width: 100%;
- max-width: 280px;
- height: auto;
- border-radius: 4px;
- border: 1px solid var(--border);
- }
-
- .contact-box {
- border: 1px solid var(--border);
- padding: 12px;
- max-width: 280px;
- }
-
- .box-title {
- color: var(--accent);
- font-size: 12px;
- margin-bottom: 8px;
- text-align: center;
- }
-
- .contact-item {
- display: flex;
- margin-bottom: 4px;
- font-size: 12px;
- min-width: 0;
- }
-
- .contact-item .label {
- color: var(--fg-dim);
- width: 70px;
- flex-shrink: 0;
- }
-
- .contact-item a {
- color: var(--light-blue);
- text-decoration: none;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- direction: rtl;
- text-align: left;
- }
-
- .contact-item a:hover {
- opacity: 0.8;
- }
-
- .section {
- flex-shrink: 0;
- }
-
- .section h1 {
- font-size: 18px;
- font-weight: 400;
- margin-bottom: 4px;
- }
-
- .section h1 .dim {
- color: var(--fg-dim);
- font-size: 14px;
- }
-
- .section .bio {
- color: var(--fg-dim);
- font-style: italic;
- }
-
- .info-columns {
- display: flex;
- gap: 48px;
- }
-
- .info-column {
- flex: 1;
- }
-
- .info-row {
- display: flex;
- font-size: 13px;
- margin-bottom: 4px;
- }
-
- .info-row .label {
- color: var(--fg-dim);
- width: 100px;
- flex-shrink: 0;
- }
-
- .info-row a {
- color: var(--light-blue);
- text-decoration: none;
- }
-
- .info-row a:hover {
- opacity: 0.8;
- }
-
- .tags {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- }
-
- .weekly-stats {
- background: var(--bg-alt);
- border: 1px solid var(--border);
- padding: 12px;
- }
-
- .weekly-total {
- font-size: 16px;
- font-weight: 500;
- color: var(--accent);
- margin-bottom: 12px;
- }
-
- .weekly-days {
- display: flex;
- flex-direction: column;
- gap: 6px;
- }
-
- .weekly-day-row {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 12px;
- }
-
- .weekly-day-name {
- width: 32px;
- flex-shrink: 0;
- color: var(--fg-dim);
- }
-
- .weekly-day-bar-container {
- flex: 1;
- height: 8px;
- background: var(--bg);
- border-radius: 2px;
- overflow: hidden;
- }
-
- .weekly-day-bar {
- height: 100%;
- background: var(--accent);
- border-radius: 2px;
- }
-
- .weekly-day-time {
- min-width: 50px;
- flex-shrink: 0;
- text-align: right;
- color: var(--fg-dim);
- white-space: nowrap;
- }
-
- @media (max-width: 768px) {
- .about-layout {
- flex-direction: column;
- }
-
- .about-left {
- display: flex;
- flex-direction: row;
- width: 100%;
- border-right: none;
- border-bottom: 1px solid var(--border);
- padding: 16px;
- gap: 16px;
- }
-
- .profile-image {
- width: 120px;
- min-width: 60px;
- flex-shrink: 1;
- height: 120px;
- align-self: flex-start;
- object-fit: cover;
- object-position: center;
- }
-
- .contact-box {
- flex: 1;
- max-width: none;
- min-width: 0;
- }
-
- .contact-item .label {
- width: 70px;
- }
-
- .contact-item a {
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .about-right {
- padding: 16px;
- display: block;
- }
-
- .section {
- margin-bottom: 16px;
- }
-
- .section h1 {
- font-size: 16px;
- }
-
- .section h1 .dim {
- font-size: 12px;
- }
-
- .info-columns {
- flex-direction: column;
- gap: 12px;
- }
-
- .tags {
- gap: 4px;
- }
-
- .tag {
- font-size: 11px;
- padding: 2px 6px;
- }
-
- .weekly-stats {
- padding: 10px;
- }
-
- .weekly-total {
- font-size: 14px;
- }
-
- .weekly-day-row {
- font-size: 11px;
- }
-
- .weekly-day-name {
- width: 28px;
- }
-
- .weekly-day-time {
- min-width: 45px;
- }
- }
-
- @media (max-width: 480px) {
- .contact-item .label {
- display: none;
- }
- }
-</style>
diff --git a/src/layouts/tabs/Blog.astro b/src/layouts/tabs/Blog.astro
@@ -1,230 +0,0 @@
----
-import BlogArticle from "../components/Article.astro";
-
-import { blogs } from "../../blogs";
----
-
-<section id="blog" class="page">
- {
- blogs.length === 0 ? (
- <div class="blog-empty">
- <p class="color-dim">No blog posts yet.</p>
- <p class="color-dim">Check back soon!</p>
- </div>
- ) : (
- <>
- <div id="blog-list-view" class="blog-list-view">
- <div class="blog-list-container">
- {blogs.map((blog, i) => (
- <div class="blog-list-item" data-index={i} data-id={blog.id}>
- <span class="blog-list-date">{blog.date}</span>
- <span class="blog-list-title">{blog.title}</span>
- <span class="blog-list-arrow">→</span>
- </div>
- ))}
- </div>
- </div>
-
- <div id="blog-post-view" class="blog-post-view hidden">
- <div class="blog-post-container">
- <button class="blog-back-btn">← Back to posts</button>
- {blogs.map((blog, i) => (
- <div
- class="blog-post hidden"
- data-index={i}
- data-id={blog.id}
- data-blog-id={blog.id}
- >
- <BlogArticle blog={blog} />
- </div>
- ))}
- </div>
- </div>
- </>
- )
- }
-</section>
-
-<style>
- .blog-empty {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 100%;
- width: 100%;
- gap: 8px;
- }
-
- .blog-list-view {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100%;
- width: 100%;
- }
-
- .blog-list-view.hidden {
- display: none;
- }
-
- .blog-list-container {
- max-width: 600px;
- width: 100%;
- padding: 32px;
- }
-
- .blog-list-item {
- display: flex;
- align-items: center;
- gap: 16px;
- padding: 16px 20px;
- border: 1px solid var(--border);
- margin-bottom: 12px;
- cursor: pointer;
- transition:
- background 0.15s ease,
- border-color 0.15s ease;
- text-decoration: none;
- color: inherit;
- }
-
- .blog-list-item:hover {
- background: var(--selection);
- border-color: var(--accent);
- }
-
- .blog-list-item.selected {
- background: var(--bg-alt);
- border-color: var(--accent);
- }
-
- .blog-list-item.selected:hover {
- background: var(--selection);
- }
-
- .blog-list-item:last-child {
- margin-bottom: 0;
- }
-
- .blog-list-date {
- color: var(--fg-dim);
- font-size: 12px;
- flex-shrink: 0;
- width: 90px;
- }
-
- .blog-list-title {
- flex: 1;
- color: var(--fg);
- font-weight: 500;
- }
-
- .blog-list-arrow {
- color: var(--fg-dim);
- font-size: 14px;
- transition:
- transform 0.15s,
- color 0.15s;
- }
-
- .blog-list-item:hover .blog-list-arrow {
- color: var(--accent);
- transform: translateX(4px);
- }
-
- .blog-post-view {
- display: flex;
- justify-content: center;
- height: 100%;
- width: 100%;
- overflow-y: auto;
- padding: 32px;
- }
-
- .blog-post-view.hidden {
- display: none;
- }
-
- .blog-post-container {
- max-width: 700px;
- width: 100%;
- }
-
- .blog-post.hidden {
- display: none;
- }
-
- .blog-back-btn {
- display: inline-block;
- background: transparent;
- border: 1px solid var(--border);
- color: var(--fg-dim);
- padding: 8px 16px;
- cursor: pointer;
- font-family: inherit;
- font-size: 12px;
- margin-bottom: 24px;
- transition: all 0.15s;
- text-decoration: none;
- }
-
- .blog-back-btn:hover {
- background: var(--bg-alt);
- color: var(--accent);
- border-color: var(--accent);
- }
-
- @media (max-width: 768px) {
- .blog-list-container {
- padding: 16px;
- }
-
- .blog-list-item {
- gap: 12px;
- padding: 12px 16px;
- margin-bottom: 10px;
- }
-
- .blog-list-date {
- font-size: 11px;
- width: 80px;
- }
-
- .blog-list-title {
- font-size: 14px;
- }
-
- .blog-post-view {
- padding: 16px;
- }
-
- .blog-back-btn {
- padding: 6px 12px;
- font-size: 11px;
- margin-bottom: 16px;
- }
- }
-
- @media (max-width: 480px) {
- .blog-list-item {
- flex-wrap: wrap;
- gap: 4px;
- }
-
- .blog-list-date {
- width: auto;
- order: 2;
- }
-
- .blog-list-title {
- width: 100%;
- order: 1;
- }
-
- .blog-list-arrow {
- order: 3;
- margin-left: auto;
- }
- }
-</style>
diff --git a/src/layouts/tabs/Experience.astro b/src/layouts/tabs/Experience.astro
@@ -1,488 +0,0 @@
----
-import { experience } from "../../config";
-import { calculateMonths } from "../../utils/period";
-import { MONTH_NAMES } from "../../constants/calendar";
-import { getTagClass } from "../../utils/tags";
-
-const dates = experience.map((exp) => {
- const [year, month] = exp.startDate.split("-").map(Number);
- return new Date(year, month - 1, 1);
-});
-const endDates = experience.map((exp) =>
- exp.endDate
- ? new Date(
- parseInt(exp.endDate.split("-")[0]),
- parseInt(exp.endDate.split("-")[1]) - 1,
- 28,
- )
- : new Date(),
-);
-const earliestDate = new Date(Math.min(...dates.map((d) => d.getTime())));
-const latestDate = new Date(Math.max(...endDates.map((d) => d.getTime())));
-
-const timelineStart = new Date(
- earliestDate.getFullYear(),
- earliestDate.getMonth(),
- 1,
-);
-const timelineEnd = latestDate;
-
-const monthTicks: string[] = [];
-let current = new Date(timelineStart);
-while (current <= timelineEnd) {
- monthTicks.push(MONTH_NAMES[current.getMonth()][0]);
- current.setMonth(current.getMonth() + 1);
-}
----
-
-<section id="experience" class="page">
- <article class="experience-layout">
- <div class="experience-detail">
- {
- experience.map((exp, index) => {
- const months = calculateMonths(exp.startDate, exp.endDate);
- const [startYear, startMonth] = exp.startDate.split("-").map(Number);
- const startStr = `${MONTH_NAMES[startMonth - 1]} ${startYear}`;
- const endStr = exp.endDate
- ? `${MONTH_NAMES[parseInt(exp.endDate.split("-")[1]) - 1]} ${exp.endDate.split("-")[0]}`
- : "Present";
-
- const isLast = index === experience.length - 1;
-
- return (
- <div class="exp-row" data-index={index} data-id={exp.id}>
- <div class="exp-card" data-index={index} data-id={exp.id}>
- <div class="exp-card-header">
- <div class="exp-card-title">
- {exp.logo ? (
- <div class="exp-title-with-logo">
- <img
- src={exp.logo}
- alt={`${exp.company} logo`}
- class={`exp-logo ${exp.logoFilter || ""}`}
- />
- <span class="exp-role">{exp.role}</span>
- </div>
- ) : (
- <div class="exp-title-text">
- <h2>{exp.company}</h2>
- <span class="exp-role">{exp.role}</span>
- </div>
- )}
- </div>
- <div class="exp-card-meta">
- <span class="exp-dates">
- {startStr} → {endStr}
- </span>
- <span class="exp-duration">
- {months} {months === 1 ? "month" : "months"}
- </span>
- {exp.url && (
- <a
- class="external-link"
- href={exp.url}
- target="_blank"
- rel="noopener noreferrer"
- >
- {new URL(exp.url).hostname} →
- </a>
- )}
- </div>
- </div>
- <div class="exp-card-tech">
- {exp.tech.map((tech) => (
- <span class={`tag ${getTagClass(tech)}`}>{tech}</span>
- ))}
- </div>
- <div class="exp-card-body">
- {exp.description.map((description) => (
- <p>{description}</p>
- ))}
- </div>
- {exp.images && exp.images.length > 0 && (
- <div class="exp-card-images">
- <img
- src={exp.images[0]}
- alt={`${exp.company} workplace, ${exp.role} experience at ${exp.company}`}
- class="exp-image-hero"
- loading="lazy"
- data-idx="0"
- />
- {exp.images.length > 1 && (
- <div class="exp-image-grid">
- {exp.images.slice(1).map((image, index) => (
- <img
- src={image}
- alt={`${exp.company} team and office environment, image ${index + 2} of ${exp.images!.length}`}
- class="exp-image-thumb"
- loading="lazy"
- data-idx={index + 1}
- />
- ))}
- </div>
- )}
- </div>
- )}
- </div>
- <div
- class={`exp-timeline ${index === 0 ? "active" : ""}`}
- data-index={index}
- >
- <span class="timeline-dot" />
- <span class="timeline-year">
- [{exp.endDate ? exp.endDate.split("-")[0] : "Now"}]
- </span>
- {!isLast && <div class="timeline-line" />}
- </div>
- </div>
- );
- })
- }
- </div>
- </article>
-</section>
-
-<style>
- .experience-layout {
- display: flex;
- flex-direction: row;
- height: 100%;
- width: 100%;
- padding: 24px 32px;
- gap: 24px;
- }
-
- .experience-detail {
- flex: 1;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- gap: 16px;
- padding: 4px;
- }
-
- .exp-card {
- background: var(--bg-alt);
- border: 2px solid var(--border);
- border-radius: 4px;
- padding: 24px;
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 0;
- scroll-snap-align: start;
- transition: border-color 0.2s ease;
- }
-
- .exp-card.selected {
- border-color: var(--accent);
- }
-
- .exp-row {
- display: flex;
- }
-
- .exp-timeline {
- width: 64px;
- flex-shrink: 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- padding-top: 24px;
- position: relative;
- }
-
- .timeline-dot {
- width: 12px;
- height: 12px;
- border: 2px solid var(--fg-dim);
- border-radius: 50%;
- background: transparent;
- transition: all 0.2s ease;
- font-size: 0;
- }
-
- .exp-timeline.active .timeline-dot {
- border-color: var(--accent);
- background: var(--accent);
- transform: scale(1.2);
- }
-
- .timeline-year {
- font-size: 10px;
- color: var(--fg-dim);
- font-family: monospace;
- margin-top: 4px;
- transition: color 0.2s ease;
- }
-
- .exp-timeline.active .timeline-year {
- color: var(--accent);
- }
-
- .timeline-line {
- position: absolute;
- top: 56px;
- bottom: -16px;
- width: 2px;
- left: 50%;
- transform: translateX(-50%);
- background: repeating-linear-gradient(
- to bottom,
- var(--border) 0px,
- var(--border) 4px,
- transparent 4px,
- transparent 8px
- );
- }
-
- .exp-card-header {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- margin-bottom: 16px;
- gap: 24px;
- }
-
- .exp-card-title {
- display: flex;
- align-items: center;
- gap: 16px;
- }
-
- .exp-logo {
- height: 32px;
- width: auto;
- flex-shrink: 0;
- }
-
- .exp-logo.logo-invert {
- filter: brightness(0) invert(1);
- }
-
- .exp-title-with-logo {
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
-
- .exp-title-with-logo .exp-role {
- color: #999;
- font-size: 14px;
- }
-
- .exp-title-with-logo .exp-duration {
- color: #aaa;
- font-size: 12px;
- }
-
- .exp-title-text h2 {
- font-size: 22px;
- font-weight: 500;
- color: var(--accent);
- margin: 0 0 4px 0;
- }
-
- .exp-title-text .exp-role {
- font-size: 14px;
- color: #999;
- }
-
- .exp-card-meta {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- gap: 4px;
- font-size: 13px;
- }
-
- .exp-dates {
- color: var(--fg);
- }
-
- .exp-duration {
- color: #aaa;
- font-size: 12px;
- }
-
- .exp-card-tech {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- margin-bottom: 20px;
- }
-
- .exp-card-body {
- color: var(--fg);
- line-height: 1.7;
- padding-bottom: 0;
- margin-bottom: 0;
- }
-
- .exp-card-body p {
- margin-bottom: 12px;
- }
-
- .exp-card-body p:last-child {
- margin-bottom: 0;
- }
-
- .exp-card-images {
- display: flex;
- gap: 12px;
- margin-top: 12px;
- padding-top: 12px;
- border-top: 1px solid var(--border);
- max-height: 180px;
- overflow: hidden;
- }
-
- .exp-image-hero {
- max-width: 280px;
- height: 100%;
- border-radius: 4px;
- border: 1px solid var(--border);
- object-fit: cover;
- cursor: pointer;
- transition:
- transform 0.15s,
- box-shadow 0.15s;
- }
-
- .exp-image-hero:hover {
- opacity: 0.8;
- border-color: var(--accent);
- }
-
- .exp-image-grid {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- max-width: 400px;
- }
-
- .exp-image-thumb {
- width: calc(50% - 4px);
- height: 86px;
- border-radius: 4px;
- border: 1px solid var(--border);
- object-fit: cover;
- cursor: pointer;
- transition:
- opacity 0.15s,
- border-color 0.15s;
- }
-
- .exp-image-thumb:hover {
- opacity: 0.8;
- border-color: var(--accent);
- }
-
- .external-link {
- color: var(--light-blue);
- text-decoration: none;
- font-size: 12px;
- }
-
- .external-link:hover {
- opacity: 0.8;
- }
-
- @media (max-width: 768px) {
- .experience-layout {
- padding: 16px;
- gap: 12px;
- }
-
- .exp-timeline {
- display: none;
- }
-
- .exp-card {
- padding: 16px;
- }
-
- .exp-card.selected {
- border-color: var(--border);
- }
-
- .exp-card-header {
- flex-direction: column;
- gap: 12px;
- }
-
- .exp-card-meta {
- align-items: flex-start;
- }
-
- .exp-logo {
- height: 28px;
- }
-
- .exp-title-text h2 {
- font-size: 18px;
- }
-
- .exp-card-tech {
- gap: 4px;
- }
-
- .exp-card-body {
- font-size: 13px;
- }
-
- .exp-card-images {
- flex-direction: column;
- gap: 12px;
- max-height: none;
- overflow: visible;
- }
-
- .exp-image-hero {
- max-width: 100%;
- height: auto;
- max-height: 200px;
- }
-
- .exp-image-grid {
- width: 100%;
- max-width: none;
- flex-direction: row;
- flex-wrap: wrap;
- gap: 8px;
- }
-
- .exp-image-thumb {
- width: calc(33.33% - 6px);
- height: 80px;
- }
- }
-
- @media (max-width: 480px) {
- .exp-image-thumb {
- width: calc(50% - 4px);
- }
- }
-</style>
-
-<script>
- document
- .querySelectorAll(".exp-image-hero, .exp-image-thumb")
- .forEach((img) => {
- img.addEventListener("click", () => {
- const container = (img as HTMLElement).closest(".exp-card-images");
- const allImages = container
- ? Array.from(container.querySelectorAll("img")).map(
- (i) => (i as HTMLImageElement).src,
- )
- : [];
- const startIndex = allImages.indexOf((img as HTMLImageElement).src);
- (
- window as Window & {
- openLightbox?: (images: string[], startIndex?: number) => void;
- }
- ).openLightbox?.(allImages, startIndex);
- });
- });
-</script>
diff --git a/src/layouts/tabs/Projects.astro b/src/layouts/tabs/Projects.astro
@@ -1,411 +0,0 @@
----
-import { projects } from "../../config/projects";
-import { fetchGitMetadataForProject } from "../../external/git";
-import { getTechMeta, getTagClass } from "../../utils/tags";
-import { parseMarkdown, removeFirstH1 } from "../../utils/markdown";
-
-const metadata = Object.fromEntries(
- (
- await Promise.all(
- projects.map(async (project) => [
- project.id,
- await fetchGitMetadataForProject(project),
- ]),
- )
- ).filter(([, meta]) => meta !== null),
-);
----
-
-<section id="projects" class="page">
- <aside class="list-view" aria-label="Project list">
- {
- projects.map((prg, i) => (
- <div
- class={`list-item ${i === 0 ? "selected" : ""}`}
- data-index={i}
- data-id={prg.id}
- >
- <div class="list-header">
- <span class="list-title">{prg.name}</span>
- <span class="list-meta">{getTechMeta(prg.tech)}</span>
- </div>
- <div class="list-subtitle">{prg.subtitle}</div>
- </div>
- ))
- }
- </aside>
- <article class="detail-view">
- {
- projects.map((prg, index) => (
- <div
- class={`detail-content ${index !== 0 ? "hidden" : ""}`}
- data-for={index}
- data-id={prg.id}
- >
- <div class="detail-header">
- <h2>{prg.name}</h2>
- <a
- class="external-link"
- href={prg.url}
- target="_blank"
- rel="noopener noreferrer"
- >
- {prg.url.includes("github.com") ? "GitHub" : "GitLab"} →
- </a>
- </div>
- <div class="tech-stack">
- {prg.tech.map((tech) => (
- <span class={`tag ${getTagClass(tech)}`}>{tech}</span>
- ))}
- </div>
- <div class="readme-content" data-readme={prg.id}>
- {metadata?.[prg.id]?.readme ? (
- <Fragment
- set:html={parseMarkdown(removeFirstH1(metadata[prg.id].readme))}
- />
- ) : (
- <p class="color-dim">README not available</p>
- )}
- </div>
- </div>
- ))
- }
- </article>
-</section>
-
-<style>
- aside.list-view {
- width: 300px;
- flex-shrink: 0;
- border-right: 1px solid var(--border);
- overflow-y: auto;
- }
-
- article.detail-view {
- flex: 1;
- padding: 24px 32px;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- }
-
- .list-item {
- padding: 12px 16px;
- border-bottom: 1px solid var(--border);
- cursor: pointer;
- transition: background 0.1s;
- }
-
- .list-item:hover {
- background: var(--bg-alt);
- }
-
- .list-item.selected {
- background: var(--selection);
- border-left: 3px solid var(--accent);
- }
-
- .list-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 2px;
- }
-
- .list-title {
- font-weight: 600;
- color: var(--fg);
- }
-
- .list-meta {
- font-size: 11px;
- color: var(--fg-dim);
- }
-
- .list-subtitle {
- font-size: 12px;
- color: var(--fg-dim);
- }
-
- .detail-content {
- display: flex;
- flex-direction: column;
- height: 100%;
- }
-
- .detail-content.hidden {
- display: none;
- }
-
- .detail-content h2 {
- font-size: 20px;
- font-weight: 400;
- color: var(--accent);
- margin-bottom: 4px;
- }
-
- .detail-content .tech-stack {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- margin-bottom: 16px;
- }
-
- .detail-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12px;
- }
-
- .detail-header h2 {
- margin-bottom: 0;
- }
-
- .external-link {
- color: var(--light-blue);
- text-decoration: none;
- font-size: 12px;
- }
-
- .external-link:hover {
- opacity: 0.8;
- }
-
- .readme-content {
- flex: 1;
- min-height: 0;
- overflow-y: auto;
- font-size: 13px;
- line-height: 1.6;
- color: var(--fg);
- margin-top: 16px;
- padding-top: 16px;
- padding-right: 12px;
- border-top: 1px solid var(--border);
- }
-
- .readme-content :global(p) {
- margin-bottom: 12px;
- }
-
- .readme-content :global(p:last-child) {
- margin-bottom: 0;
- }
-
- .readme-content :global(h2),
- .readme-content :global(.readme-h1) {
- font-size: 16px;
- color: var(--accent);
- margin: 20px 0 8px;
- font-weight: 600;
- }
-
- .readme-content :global(h2:first-child),
- .readme-content :global(.readme-h1:first-child) {
- margin-top: 0;
- }
-
- .readme-content :global(h3),
- .readme-content :global(.readme-h2) {
- font-size: 14px;
- color: var(--accent);
- margin: 16px 0 8px;
- font-weight: 500;
- }
-
- .readme-content :global(h4),
- .readme-content :global(.readme-h3) {
- font-size: 13px;
- color: var(--fg);
- margin: 12px 0 6px;
- font-weight: 500;
- }
-
- .readme-content :global(h5),
- .readme-content :global(h6) {
- font-size: 12px;
- color: var(--fg-dim);
- margin: 10px 0 6px;
- font-weight: 500;
- }
-
- .readme-content :global(pre) {
- display: block;
- background: var(--bg-alt);
- border: 1px solid var(--border);
- padding: 12px;
- margin: 12px 0;
- overflow-x: auto;
- font-size: 12px;
- line-height: 1.4;
- }
-
- .readme-content :global(pre code) {
- background: none;
- padding: 0;
- font-size: inherit;
- }
-
- .readme-content :global(code) {
- background: var(--bg-alt);
- padding: 2px 6px;
- font-size: 12px;
- border-radius: 2px;
- }
-
- .readme-content :global(a) {
- color: var(--light-blue);
- text-decoration: none;
- }
-
- .readme-content :global(a:hover) {
- opacity: 0.8;
- }
-
- .readme-content :global(ul),
- .readme-content :global(ol) {
- margin: 8px 0;
- padding-left: 20px;
- }
-
- .readme-content :global(li) {
- margin-bottom: 4px;
- }
-
- .readme-content :global(li > ul),
- .readme-content :global(li > ol) {
- margin: 4px 0;
- }
-
- .readme-content :global(table) {
- width: 100%;
- border-collapse: collapse;
- margin: 12px 0;
- font-size: 12px;
- }
-
- .readme-content :global(th),
- .readme-content :global(td) {
- border: 1px solid var(--border);
- padding: 8px 12px;
- text-align: left;
- }
-
- .readme-content :global(th) {
- background: var(--bg-alt);
- color: var(--accent);
- font-weight: 500;
- }
-
- .readme-content :global(tr:nth-child(even)) {
- background: rgba(255, 255, 255, 0.02);
- }
-
- .readme-content :global(blockquote) {
- border-left: 3px solid var(--accent);
- margin: 12px 0;
- padding: 8px 16px;
- color: var(--fg-dim);
- background: var(--bg-alt);
- }
-
- .readme-content :global(blockquote p) {
- margin-bottom: 0;
- }
-
- .readme-content :global(hr) {
- border: none;
- border-top: 1px solid var(--border);
- margin: 20px 0;
- }
-
- .readme-content :global(img) {
- max-width: min(720px, 100%);
- height: auto;
- margin: 12px 0;
- }
-
- .readme-content :global(strong) {
- color: var(--fg);
- font-weight: 600;
- }
-
- .readme-content :global(em) {
- font-style: italic;
- color: var(--fg-dim);
- }
-
- .readme-content :global(del) {
- text-decoration: line-through;
- color: var(--fg-dim);
- }
-
- @media (max-width: 768px) {
- #projects {
- flex-direction: column;
- }
-
- aside.list-view {
- width: 100%;
- border-right: none;
- border-bottom: 1px solid var(--border);
- max-height: 200px;
- overflow-y: auto;
- }
-
- .list-item {
- padding: 10px 14px;
- }
-
- .list-title {
- font-size: 13px;
- }
-
- .list-subtitle {
- font-size: 11px;
- }
-
- article.detail-view {
- padding: 16px;
- overflow-y: auto;
- }
-
- .detail-content h2 {
- font-size: 18px;
- }
-
- .detail-header {
- flex-direction: column;
- align-items: flex-start;
- gap: 8px;
- }
-
- .readme-content {
- font-size: 12px;
- }
-
- .readme-content :global(pre) {
- font-size: 11px;
- padding: 10px;
- }
-
- .readme-content :global(code) {
- font-size: 11px;
- }
- }
-
- @media (max-width: 480px) {
- aside.list-view {
- max-height: 150px;
- }
-
- .list-item {
- padding: 8px 12px;
- }
-
- .list-meta {
- display: none;
- }
- }
-</style>
diff --git a/src/pages/404.astro b/src/pages/404.astro
@@ -1,83 +0,0 @@
----
-import Layout from "../layouts/Layout.astro";
-
-import { about } from "../config";
-import { SITE_URL } from "../constants/website";
----
-
-<Layout
- title={`404 - Page Not Found | ${about.name}`}
- description="The page you're looking for doesn't exist."
- type="website"
- url={`${SITE_URL}/404`}
->
- <main class="error-page">
- <div class="error-content">
- <h1>404</h1>
- <p class="error-message">Page not found</p>
- <p class="error-description">
- The page you're looking for doesn't exist or has been moved.
- </p>
- <a href="/" class="home-link">← Return home</a>
- </div>
- </main>
-</Layout>
-
-<style>
- .error-page {
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: 100vh;
- padding: 24px;
- }
-
- .error-content {
- text-align: center;
- }
-
- h1 {
- font-size: 72px;
- font-weight: 400;
- color: var(--accent);
- margin-bottom: 8px;
- }
-
- .error-message {
- font-size: 24px;
- color: var(--fg);
- margin-bottom: 16px;
- }
-
- .error-description {
- color: var(--fg-dim);
- margin-bottom: 32px;
- }
-
- .home-link {
- display: inline-block;
- color: var(--light-blue);
- padding: 8px 16px;
- border: 1px solid var(--border);
- transition: border-color 0.2s;
- }
-
- .home-link:hover {
- border-color: var(--light-blue);
- opacity: 1;
- }
-
- @media (max-width: 768px) {
- h1 {
- font-size: 48px;
- }
-
- .error-message {
- font-size: 18px;
- }
- }
-</style>
-
-<script>
- document.getElementById("app")?.classList.add("ready");
-</script>
diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[slug].astro
@@ -1,118 +0,0 @@
----
-import Layout from "../../layouts/Layout.astro";
-import Article from "../../layouts/components/Article.astro";
-
-import { blogs } from "../../blogs";
-import schemas from "../../schemas/index";
-import { SITE_URL } from "../../constants/website";
-
-const blog = blogs.find((blog) => blog.id === Astro.params.slug);
-if (!blog) {
- return new Response(null, {
- status: 404,
- statusText: "Not Found",
- });
-}
-
-const keywords = blog.tags?.join(", ");
-
-const { getSchemaForBlog, getBreadcrumbSchema } = schemas.blog;
----
-
-<Layout
- title={`${blog.title} | Damian Myrda`}
- description={blog.description}
- type="article"
- url={`${SITE_URL}/blog/${blog.id}`}
- publishedTime={blog.date}
- modifiedTime={blog.modifiedDate || blog.date}
- keywords={keywords}
- image={blog.image}
->
- <main id="content">
- <div class="blog-page">
- <a href="/#blog" class="blog-back-btn">← See more posts</a>
- <Article blog={blog} />
- </div>
- <div id="blog-data" data-blog-id={blog.id}></div>
- <script
- type="application/ld+json"
- set:html={JSON.stringify(getSchemaForBlog(blog))}
- />
- <script
- type="application/ld+json"
- set:html={JSON.stringify(getBreadcrumbSchema(blog))}
- />
- </main>
-</Layout>
-
-<script>
- document.getElementById("app")?.classList.add("ready");
-</script>
-
-<style>
- :global(body) {
- overflow: auto;
- }
-
- :global(#app) {
- height: auto;
- min-height: 100vh;
- }
-
- :global(#content) {
- overflow: visible;
- }
-
- .blog-page {
- max-width: 720px;
- margin: 0 auto;
- padding: 40px 24px 0;
- min-height: 100vh;
- }
-
- .blog-back-btn {
- display: inline-block;
- background: transparent;
- border: 1px solid var(--border);
- color: var(--fg-dim);
- padding: 8px 16px;
- cursor: pointer;
- font-family: inherit;
- font-size: 12px;
- margin-bottom: 24px;
- transition: all 0.15s;
- text-decoration: none;
- }
-
- .blog-back-btn:hover {
- background: var(--bg-alt);
- color: var(--accent);
- border-color: var(--accent);
- }
-
- @media (max-width: 768px) {
- .blog-back-btn {
- padding: 6px 12px;
- font-size: 11px;
- margin-bottom: 16px;
- }
- }
-</style>
-
-<script>
- import { initKeyboard, onKey } from "../../ui/keyboard";
- import { SCROLL_AMOUNT } from "../../constants/ui/scroll";
-
- initKeyboard();
-
- onKey("ArrowUp", (e) => {
- e.preventDefault();
- window.scrollBy({ top: -SCROLL_AMOUNT, behavior: "smooth" });
- });
-
- onKey("ArrowDown", (e) => {
- e.preventDefault();
- window.scrollBy({ top: SCROLL_AMOUNT, behavior: "smooth" });
- });
-</script>
diff --git a/src/pages/experience/[id].astro b/src/pages/experience/[id].astro
@@ -1,376 +0,0 @@
----
-import Layout from "../../layouts/Layout.astro";
-import Lightbox from "../../layouts/components/Lightbox.astro";
-
-import { SITE_URL } from "../../constants/website";
-import { experience as experiences } from "../../config";
-import { calculateMonths } from "../../utils/period";
-import { MONTH_NAMES } from "../../constants/calendar";
-import { getTagClass } from "../../utils/tags";
-import schemas from "../../schemas/index";
-
-const exp = experiences.find((e) => e.id === Astro.params.id);
-if (!exp) {
- return new Response(null, {
- status: 404,
- statusText: "Not Found",
- });
-}
-
-const months = calculateMonths(exp.startDate, exp.endDate);
-const [startYear, startMonth] = exp.startDate.split("-").map(Number);
-const startStr = `${MONTH_NAMES[startMonth - 1]} ${startYear}`;
-const endStr = exp.endDate
- ? `${MONTH_NAMES[parseInt(exp.endDate.split("-")[1]) - 1]} ${exp.endDate.split("-")[0]}`
- : "Present";
-
-const keywords = [...exp.tech, exp.company, exp.role].join(", ");
-const description = `${exp.role} at ${exp.company}. ${exp.description[0]}`;
-
-const { getSchemaForExperience, getBreadcrumbSchema } = schemas.experience;
----
-
-<Layout
- title={`${exp.company} - ${exp.role} | Damian Myrda`}
- description={description}
- type="website"
- url={`${SITE_URL}/experience/${exp.id}`}
- keywords={keywords}
- image={exp.logo}
->
- <main id="content">
- <div class="experience-page">
- <a href="/#experience" class="back-btn">← See more experience</a>
-
- <article class="experience-article">
- <header class="article-header">
- <div class="header-top">
- <div class="title-section">
- <div class="exp-title-with-logo">
- <img
- src={exp.logo}
- alt={exp.company}
- class={`exp-logo ${exp.logoFilter || ""}`}
- />
- <span class="exp-role">{exp.role}</span>
- </div>
- </div>
- <div class="exp-card-meta">
- <span class="exp-dates">{startStr} → {endStr}</span>
- <span class="exp-duration">{months} months</span>
- {
- exp.url && (
- <a
- class="external-link"
- href={exp.url}
- target="_blank"
- rel="noopener noreferrer"
- >
- {new URL(exp.url).hostname} →
- </a>
- )
- }
- </div>
- </div>
- </header>
-
- <section class="tech-tags">
- {
- exp.tech.map((tech) => (
- <span class={`tag ${getTagClass(tech)}`}>{tech}</span>
- ))
- }
- </section>
-
- <section class="article-content">
- {exp.description.map((paragraph) => <p>{paragraph}</p>)}
- </section>
-
- {
- exp.images && exp.images.length > 0 && (
- <section class="images-section">
- <h2>Photos</h2>
- <div class="image-grid">
- {exp.images.map((img, i) => (
- <img
- src={img}
- alt={`${exp.company} photo ${i + 1}`}
- class="experience-image"
- loading="lazy"
- />
- ))}
- </div>
- </section>
- )
- }
- </article>
- </div>
- <script
- type="application/ld+json"
- set:html={JSON.stringify(getSchemaForExperience(exp))}
- />
- <script
- type="application/ld+json"
- set:html={JSON.stringify(getBreadcrumbSchema(exp))}
- />
- </main>
- <Lightbox />
-</Layout>
-
-<script>
- document.getElementById("app")?.classList.add("ready");
-
- // Lightbox click handlers for images
- document.querySelectorAll(".experience-image").forEach((img) => {
- img.addEventListener("click", () => {
- const container = (img as HTMLElement).closest(".image-grid");
- const allImages = container
- ? Array.from(container.querySelectorAll("img")).map(
- (i) => (i as HTMLImageElement).src,
- )
- : [];
- const startIndex = allImages.indexOf((img as HTMLImageElement).src);
- (
- window as Window & {
- openLightbox?: (images: string[], startIndex?: number) => void;
- }
- ).openLightbox?.(allImages, startIndex);
- });
- });
-</script>
-
-<style>
- :global(body) {
- overflow: auto;
- }
-
- :global(#app) {
- height: auto;
- min-height: 100vh;
- }
-
- :global(#content) {
- overflow: visible;
- }
-
- .experience-page {
- max-width: 720px;
- margin: 0 auto;
- padding: 40px 24px 40px;
- min-height: 100vh;
- }
-
- .back-btn {
- display: inline-block;
- background: transparent;
- border: 1px solid var(--border);
- color: var(--fg-dim);
- padding: 8px 16px;
- cursor: pointer;
- font-family: inherit;
- font-size: 12px;
- margin-bottom: 24px;
- transition: all 0.15s;
- text-decoration: none;
- }
-
- .back-btn:hover {
- background: var(--bg-alt);
- color: var(--accent);
- border-color: var(--accent);
- }
-
- .experience-article {
- animation: fadeIn 0.2s ease;
- }
-
- @keyframes fadeIn {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
- }
-
- .article-header {
- margin-bottom: 24px;
- padding-bottom: 16px;
- border-bottom: 1px solid var(--border);
- }
-
- .header-top {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 24px;
- }
-
- .title-section {
- display: flex;
- align-items: center;
- gap: 16px;
- }
-
- .exp-logo {
- height: 32px;
- width: auto;
- flex-shrink: 0;
- }
-
- .exp-logo.logo-invert {
- filter: brightness(0) invert(1);
- }
-
- .exp-title-with-logo {
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
-
- .exp-title-with-logo .exp-role {
- color: #999;
- font-size: 14px;
- }
-
- .exp-card-meta {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- gap: 4px;
- font-size: 13px;
- }
-
- .exp-dates {
- color: var(--fg);
- }
-
- .exp-duration {
- color: #aaa;
- font-size: 12px;
- }
-
- .external-link {
- color: var(--light-blue);
- text-decoration: none;
- font-size: 12px;
- }
-
- .external-link:hover {
- opacity: 0.8;
- }
-
- .tech-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- margin-bottom: 24px;
- }
-
- .article-content {
- font-size: 14px;
- line-height: 1.7;
- color: var(--fg);
- margin-bottom: 32px;
- }
-
- .article-content p {
- margin-bottom: 16px;
- }
-
- .article-content p:last-child {
- margin-bottom: 0;
- }
-
- .images-section h2 {
- font-size: 16px;
- color: var(--accent);
- margin-bottom: 16px;
- font-weight: 500;
- }
-
- .image-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 12px;
- }
-
- .experience-image {
- width: 100%;
- height: 180px;
- object-fit: cover;
- border-radius: 4px;
- border: 1px solid var(--border);
- cursor: pointer;
- transition:
- opacity 0.15s,
- border-color 0.15s;
- }
-
- .experience-image:hover {
- opacity: 0.9;
- border-color: var(--accent);
- }
-
- @media (max-width: 768px) {
- .experience-page {
- padding: 24px 16px 0;
- }
-
- .back-btn {
- padding: 6px 12px;
- font-size: 11px;
- margin-bottom: 16px;
- }
-
- .header-top {
- flex-direction: column;
- gap: 12px;
- }
-
- .title-section {
- gap: 12px;
- }
-
- .company-logo {
- height: 36px;
- }
-
- h1 {
- font-size: 22px;
- }
-
- .role {
- font-size: 14px;
- }
-
- .article-content {
- font-size: 13px;
- }
-
- .image-grid {
- grid-template-columns: 1fr;
- }
-
- .experience-image {
- height: 200px;
- }
- }
-</style>
-
-<script>
- import { initKeyboard, onKey } from "../../ui/keyboard";
- import { SCROLL_AMOUNT } from "../../constants/ui/scroll";
-
- initKeyboard();
-
- onKey("ArrowUp", (e) => {
- e.preventDefault();
- window.scrollBy({ top: -SCROLL_AMOUNT, behavior: "smooth" });
- });
-
- onKey("ArrowDown", (e) => {
- e.preventDefault();
- window.scrollBy({ top: SCROLL_AMOUNT, behavior: "smooth" });
- });
-</script>
diff --git a/src/pages/index.astro b/src/pages/index.astro
@@ -1,58 +0,0 @@
----
-import Layout from "../layouts/Layout.astro";
-import Header from "../layouts/components/Header.astro";
-import AboutTab from "../layouts/tabs/About.astro";
-import ExperienceTab from "../layouts/tabs/Experience.astro";
-import ProjectsTab from "../layouts/tabs/Projects.astro";
-import BlogTab from "../layouts/tabs/Blog.astro";
-import Footer from "../layouts/components/Footer.astro";
-
-import { about, currentJob } from "../config";
-import { fetchWakaTimeData } from "../external/wakatime";
-import { SITE_URL } from "../constants/website";
-import schemas from "../schemas/index";
-
-const wakaTimeData = await fetchWakaTimeData();
-
-const { personSchema, websiteSchema } = schemas;
----
-
-<Layout
- title={`${about.name} | ${about.profession_short}`}
- description={`${about.name} - ${about.profession}${currentJob?.company ? ` at ${currentJob.company}` : ""}`}
- type="website"
- url={SITE_URL}
- keywords={[
- about.name,
- about.profession,
- currentJob?.company,
- about.languages,
- about.interests,
- about.school,
- ]
- .flat()
- .filter(Boolean)
- .join(", ")}
->
- <Header />
-
- <main id="content">
- <AboutTab wakaTimeData={wakaTimeData} />
- <ExperienceTab />
- <ProjectsTab />
- <BlogTab />
- </main>
-
- <Footer wakaTimeData={wakaTimeData} />
-
- <script type="application/ld+json" set:html={JSON.stringify(personSchema)} />
- <script type="application/ld+json" set:html={JSON.stringify(websiteSchema)} />
-</Layout>
-
-<script>
- import { initUI } from "../ui";
-
- document.addEventListener("DOMContentLoaded", () => {
- initUI();
- });
-</script>
diff --git a/src/pages/projects/[id].astro b/src/pages/projects/[id].astro
@@ -1,296 +0,0 @@
----
-import Layout from "../../layouts/Layout.astro";
-
-import { projects } from "../../config";
-import { fetchGitMetadataForProject } from "../../external/git";
-import { SITE_URL } from "../../constants/website";
-import schemas from "../../schemas/index";
-import { getTagClass } from "../../utils/tags";
-
-import { parseMarkdown, removeFirstH1 } from "../../utils/markdown";
-const project = projects.find((p) => p.id === Astro.params.id);
-if (!project) {
- return new Response(null, {
- status: 404,
- statusText: "Not Found",
- });
-}
-
-const keywords = project.tech.join(", ");
-
-const metadata = await fetchGitMetadataForProject(project);
-
-const { getSchemaForProject, getBreadcrumbSchema } = schemas.project;
----
-
-<Layout
- title={`${project.name} | Damian Myrda`}
- description={project.subtitle}
- type="website"
- url={`${SITE_URL}/projects/${project.id}`}
- keywords={keywords}
->
- <main id="content">
- <div class="project-page">
- <a href="/#projects" class="back-btn">← See more projects</a>
-
- <header class="project-header">
- <div class="header-top">
- <h1>{project.name}</h1>
- <a
- class="external-link"
- href={project.url}
- target="_blank"
- rel="noopener noreferrer"
- >
- {project.url.includes("github.com") ? "GitHub" : "GitLab"} →
- </a>
- </div>
- <p class="subtitle">{project.subtitle}</p>
- <div class="tech-stack">
- {
- project.tech.map((tech) => (
- <span class={`tag ${getTagClass(tech)}`}>{tech}</span>
- ))
- }
- </div>
- </header>
-
- <div class="readme-content">
- {
- metadata?.readme ? (
- <Fragment
- set:html={parseMarkdown(removeFirstH1(metadata.readme))}
- />
- ) : (
- <p class="color-dim">README not available</p>
- )
- }
- </div>
- </div>
- <script
- type="application/ld+json"
- set:html={JSON.stringify(
- getSchemaForProject(project, metadata ?? undefined),
- )}
- />
- <script
- type="application/ld+json"
- set:html={JSON.stringify(getBreadcrumbSchema(project))}
- />
- </main>
-</Layout>
-
-<script>
- document.getElementById("app")?.classList.add("ready");
-</script>
-
-<style>
- :global(body) {
- overflow: auto;
- }
-
- :global(#app) {
- height: auto;
- min-height: 100vh;
- }
-
- :global(#content) {
- overflow: visible;
- }
-
- .project-page {
- max-width: 800px;
- margin: 0 auto;
- padding: 40px 24px;
- min-height: 100vh;
- }
-
- .back-btn {
- display: inline-block;
- background: transparent;
- border: 1px solid var(--border);
- color: var(--fg-dim);
- padding: 8px 16px;
- cursor: pointer;
- font-family: inherit;
- font-size: 12px;
- margin-bottom: 24px;
- transition: all 0.15s;
- text-decoration: none;
- }
-
- .back-btn:hover {
- background: var(--bg-alt);
- color: var(--accent);
- border-color: var(--accent);
- }
-
- .project-header {
- margin-bottom: 32px;
- padding-bottom: 24px;
- border-bottom: 1px solid var(--border);
- }
-
- .header-top {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 8px;
- }
-
- .project-header h1 {
- font-size: 28px;
- font-weight: 500;
- color: var(--accent);
- margin: 0;
- }
-
- .external-link {
- color: var(--light-blue);
- text-decoration: none;
- font-size: 14px;
- }
-
- .external-link:hover {
- opacity: 0.8;
- }
-
- .subtitle {
- color: var(--fg-dim);
- font-size: 14px;
- margin-bottom: 16px;
- }
-
- .tech-stack {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- }
-
- .readme-content {
- font-size: 14px;
- line-height: 1.7;
- color: var(--fg);
- padding-right: 12px;
- }
-
- .readme-content :global(p) {
- margin-bottom: 16px;
- }
-
- .readme-content :global(h2),
- .readme-content :global(.readme-h1) {
- font-size: 20px;
- color: var(--accent);
- margin: 32px 0 12px;
- font-weight: 600;
- }
-
- .readme-content :global(h2:first-child),
- .readme-content :global(.readme-h1:first-child) {
- margin-top: 0;
- }
-
- .readme-content :global(h3),
- .readme-content :global(.readme-h2) {
- font-size: 17px;
- color: var(--accent);
- margin: 24px 0 10px;
- font-weight: 500;
- }
-
- .readme-content :global(pre) {
- display: block;
- background: var(--bg-alt);
- border: 1px solid var(--border);
- padding: 16px;
- margin: 16px 0;
- overflow-x: auto;
- font-size: 13px;
- line-height: 1.5;
- }
-
- .readme-content :global(pre code) {
- background: none;
- padding: 0;
- font-size: inherit;
- }
-
- .readme-content :global(code) {
- background: var(--bg-alt);
- padding: 2px 6px;
- font-size: 13px;
- border-radius: 2px;
- }
-
- .readme-content :global(a) {
- color: var(--light-blue);
- text-decoration: none;
- }
-
- .readme-content :global(a:hover) {
- opacity: 0.8;
- }
-
- .readme-content :global(ul),
- .readme-content :global(ol) {
- margin: 12px 0;
- padding-left: 24px;
- }
-
- .readme-content :global(li) {
- margin-bottom: 8px;
- }
-
- .readme-content :global(blockquote) {
- border-left: 3px solid var(--accent);
- margin: 16px 0;
- padding: 12px 20px;
- color: var(--fg-dim);
- background: var(--bg-alt);
- }
-
- .readme-content :global(img) {
- max-width: min(1024px, 100%);
- height: auto;
- margin: 16px 0;
- }
-
- @media (max-width: 768px) {
- .project-page {
- padding: 24px 16px;
- }
-
- .header-top {
- flex-direction: column;
- align-items: flex-start;
- gap: 12px;
- }
-
- .project-header h1 {
- font-size: 22px;
- }
-
- .readme-content {
- font-size: 13px;
- }
- }
-</style>
-
-<script>
- import { initKeyboard, onKey } from "../../ui/keyboard";
- import { SCROLL_AMOUNT } from "../../constants/ui/scroll";
-
- initKeyboard();
-
- onKey("ArrowUp", (e) => {
- e.preventDefault();
- window.scrollBy({ top: -SCROLL_AMOUNT, behavior: "smooth" });
- });
-
- onKey("ArrowDown", (e) => {
- e.preventDefault();
- window.scrollBy({ top: SCROLL_AMOUNT, behavior: "smooth" });
- });
-</script>
diff --git a/src/pages/sitemap.xml.ts b/src/pages/sitemap.xml.ts
@@ -1,76 +0,0 @@
-import type { APIRoute } from "astro";
-import { fetchGitMetadataForProject } from "../external/git";
-import { experience, projects } from "../config";
-import { blogs } from "../blogs";
-import { SITE_URL } from "../constants/website";
-
-export const GET: APIRoute = async () => {
- const now = new Date().toISOString().split("T")[0];
-
- const metadata = Object.fromEntries(
- (
- await Promise.all(
- projects.map(async (project) => [
- project.id,
- await fetchGitMetadataForProject(project),
- ]),
- )
- ).filter(([, meta]) => meta !== null),
- );
-
- // Static pages
- const staticPages = [
- { url: "/", changefreq: "weekly", priority: "1.0", lastmod: now },
- ];
-
- // Project pages
- const projectPages = projects.map((prg) => ({
- url: `/projects/${prg.id}`,
- changefreq: "monthly",
- priority: "0.7",
- lastmod: metadata[prg.id]?.dateModified ?? now,
- }));
-
- // Experience pages
- const experiencePages = experience.map((exp) => ({
- url: `/experience/${exp.id}`,
- changefreq: "monthly",
- priority: "0.7",
- lastmod: exp.endDate || exp.startDate || now,
- }));
-
- // Blog posts
- const blogPages = blogs.map((blog) => ({
- url: `/blog/${blog.id}`,
- changefreq: "monthly",
- priority: "0.8",
- lastmod: blog.date,
- }));
-
- const allPages = [
- ...staticPages,
- ...projectPages,
- ...experiencePages,
- ...blogPages,
- ];
-
- const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
-<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
-${allPages
- .map(
- (page) => ` <url>
- <loc>${SITE_URL}${page.url}</loc>
- <lastmod>${page.lastmod || now}</lastmod>
- <changefreq>${page.changefreq}</changefreq>
- <priority>${page.priority}</priority>
- </url>`,
- )
- .join("\n")}
-</urlset>`;
-
- return new Response(sitemap, {
- headers: {
- "Content-Type": "application/xml",
- },
- });
-};
diff --git a/src/schemas/blog.ts b/src/schemas/blog.ts
@@ -1,60 +0,0 @@
-import type { Blog } from "../blogs";
-import { SITE_URL } from "../constants/website";
-import personSchema from "./person";
-
-export function getSchemaForBlog(blog: Blog) {
- const schema: Record<string, unknown> = {
- "@context": "https://schema.org",
- "@type": "BlogPosting",
- headline: blog.title,
- description: blog.description,
- datePublished: blog.date,
- url: `${SITE_URL}/blog/${blog.id}`,
- author: personSchema,
- publisher: personSchema,
- mainEntityOfPage: {
- "@type": "WebPage",
- "@id": `${SITE_URL}/blog/${blog.id}`,
- },
- articleSection: "Technology",
- };
-
- if (blog.image) {
- schema.image = blog.image.startsWith("http")
- ? blog.image
- : `${SITE_URL}${blog.image}`;
- }
-
- if (blog.tags && blog.tags.length > 0) {
- schema.keywords = blog.tags.join(", ");
- }
-
- return schema;
-}
-
-export function getBreadcrumbSchema(blog: Blog) {
- return {
- "@context": "https://schema.org",
- "@type": "BreadcrumbList",
- itemListElement: [
- {
- "@type": "ListItem",
- position: 1,
- name: "Home",
- item: SITE_URL,
- },
- {
- "@type": "ListItem",
- position: 2,
- name: "Blog",
- item: `${SITE_URL}/#blog`,
- },
- {
- "@type": "ListItem",
- position: 3,
- name: blog.title,
- item: `${SITE_URL}/blog/${blog.id}`,
- },
- ],
- };
-}
diff --git a/src/schemas/experience.ts b/src/schemas/experience.ts
@@ -1,54 +0,0 @@
-import type { Experience } from "../config";
-import { SITE_URL } from "../constants/website";
-import personSchema from "./person";
-
-export function getSchemaForExperience(experience: Experience) {
- return {
- "@context": "https://schema.org",
- "@type": "Person",
- name: personSchema.name,
- url: personSchema.url,
- worksFor: {
- "@type": "Organization",
- name: experience.company,
- url: experience.url,
- },
- hasOccupation: {
- "@type": "Occupation",
- name: experience.role,
- startDate: experience.startDate,
- ...(experience.endDate && { endDate: experience.endDate }),
- occupationLocation: {
- "@type": "City",
- name: "Remote",
- },
- },
- };
-}
-
-export function getBreadcrumbSchema(experience: Experience) {
- return {
- "@context": "https://schema.org",
- "@type": "BreadcrumbList",
- itemListElement: [
- {
- "@type": "ListItem",
- position: 1,
- name: "Home",
- item: SITE_URL,
- },
- {
- "@type": "ListItem",
- position: 2,
- name: "Experience",
- item: `${SITE_URL}/#experience`,
- },
- {
- "@type": "ListItem",
- position: 3,
- name: experience.company,
- item: `${SITE_URL}/experience/${experience.id}`,
- },
- ],
- };
-}
diff --git a/src/schemas/index.ts b/src/schemas/index.ts
@@ -1,13 +0,0 @@
-import * as blog from "./blog";
-import * as experience from "./experience";
-import * as personSchema from "./person";
-import * as project from "./project";
-import * as websiteSchema from "./website";
-
-export default {
- blog,
- experience,
- personSchema,
- project,
- websiteSchema,
-};
diff --git a/src/schemas/person.ts b/src/schemas/person.ts
@@ -1,22 +0,0 @@
-import { about, contact, currentJob } from "../config";
-import { SITE_URL } from "../constants/website";
-
-export default {
- "@context": "https://schema.org",
- "@type": "Person",
- name: about.name,
- alternateName: about.handle,
- email: contact.email,
- sameAs: [contact.gitlab, contact.linkedin],
- url: SITE_URL,
- image: `${SITE_URL}/me.jpg`,
- ...(currentJob && {
- jobTitle: currentJob.role,
- worksFor: {
- "@type": "Organization",
- name: currentJob.company,
- url: currentJob.url,
- },
- }),
- knowsAbout: [...about.interests],
-};
diff --git a/src/schemas/project.ts b/src/schemas/project.ts
@@ -1,46 +0,0 @@
-import type Project from "../config/projects";
-import type { GitMetadata } from "../external/git";
-import { SITE_URL } from "../constants/website";
-import personSchema from "./person";
-
-export function getSchemaForProject(project: Project, metadata?: GitMetadata) {
- return {
- "@context": "https://schema.org",
- "@type": "SoftwareSourceCode",
- name: project.name,
- description: project.subtitle,
- url: `${SITE_URL}/projects/${project.id}`,
- codeRepository: project.url,
- programmingLanguage: project.tech,
- author: personSchema,
- ...(metadata?.dateCreated && { dateCreated: metadata.dateCreated }),
- ...(metadata?.dateModified && { dateModified: metadata.dateModified }),
- };
-}
-
-export function getBreadcrumbSchema(project: Project) {
- return {
- "@context": "https://schema.org",
- "@type": "BreadcrumbList",
- itemListElement: [
- {
- "@type": "ListItem",
- position: 1,
- name: "Home",
- item: SITE_URL,
- },
- {
- "@type": "ListItem",
- position: 2,
- name: "Projects",
- item: `${SITE_URL}/#projects`,
- },
- {
- "@type": "ListItem",
- position: 3,
- name: project.name,
- item: `${SITE_URL}/projects/${project.id}`,
- },
- ],
- };
-}
diff --git a/src/schemas/website.ts b/src/schemas/website.ts
@@ -1,12 +0,0 @@
-import { about, currentJob } from "../config";
-import { SITE_URL } from "../constants/website";
-import personSchema from "./person";
-
-export default {
- "@context": "https://schema.org",
- "@type": "WebSite",
- name: about.name,
- url: SITE_URL,
- description: `Portfolio and blog of ${about.name}, a ${about.profession.toLowerCase()}${currentJob?.company ? ` at ${currentJob.company}` : ""}. ${about.bio}.`,
- author: personSchema,
-};
diff --git a/src/tabs/about.ts b/src/tabs/about.ts
@@ -1,3 +0,0 @@
-import { registerTab } from "../ui/tabs";
-
-registerTab("about", {});
diff --git a/src/tabs/blog.ts b/src/tabs/blog.ts
@@ -1,191 +0,0 @@
-import { SCROLL_AMOUNT } from "../constants/ui/scroll";
-import { $, $$, setActiveByIndex, scrollIntoViewSmooth } from "../ui/dom";
-import { onKey, type KeybindHandle } from "../ui/keyboard";
-import { setRoute } from "../ui/router";
-import { cycleTab, registerTab } from "../ui/tabs";
-
-let selectedIndex = 0;
-let currentPostId: string | null = null;
-
-let keybinds: KeybindHandle[] = [];
-
-registerTab("blog", {
- init: () => {
- selectItem(0);
- setupClicks();
- setupBackButton();
- },
-
- activate: () => {
- if (currentPostId) {
- usePostKeyboard();
- } else {
- useListKeyboard();
- }
- },
- deactivate,
-
- getRouteId: () => currentPostId || undefined,
- setRouteId: (id) => {
- if (id) {
- selectedIndex = getIndexById(id);
- selectItem(selectedIndex);
- openPost(id);
- } else {
- closePost();
- }
- },
-});
-
-function useListKeyboard(): void {
- keybinds = [
- onKey("ArrowUp", (e) => {
- e.preventDefault();
- navigate("up");
- }),
- onKey("ArrowDown", (e) => {
- e.preventDefault();
- navigate("down");
- }),
- onKey("ArrowLeft", (e) => {
- e.preventDefault();
- cycleTab("prev");
- }),
- onKey("ArrowRight", (e) => {
- e.preventDefault();
- const id = getId(selectedIndex);
- openPost(id);
- setRoute("blog", id);
- }),
- onKey("Enter", (e) => {
- e.preventDefault();
- const id = getId(selectedIndex);
- openPost(id);
- setRoute("blog", id);
- }),
- ];
-}
-
-function usePostKeyboard(): void {
- keybinds = [
- onKey("ArrowUp", (e) => {
- e.preventDefault();
- scrollPost("up");
- }),
- onKey("ArrowDown", (e) => {
- e.preventDefault();
- scrollPost("down");
- }),
- onKey("ArrowLeft", (e) => {
- e.preventDefault();
- closePost();
- setRoute("blog");
- }),
- onKey("Escape", (e) => {
- e.preventDefault();
- closePost();
- setRoute("blog");
- }),
- ];
-}
-
-function openPost(id: string): void {
- currentPostId = id;
-
- const listView = $("#blog-list-view");
- const postView = $("#blog-post-view");
-
- if (listView && postView) {
- listView.classList.add("hidden");
- postView.classList.remove("hidden");
-
- $$(".blog-post").forEach((post) => {
- post.classList.toggle("hidden", post.getAttribute("data-blog-id") !== id);
- });
-
- postView.scrollTop = 0;
- }
-
- // Switch keyboard context
- deactivate();
- usePostKeyboard();
-}
-
-function closePost(): void {
- currentPostId = null;
-
- const listView = $("#blog-list-view");
- const postView = $("#blog-post-view");
-
- if (listView && postView) {
- listView.classList.remove("hidden");
- postView.classList.add("hidden");
- }
-
- // Switch keyboard context
- deactivate();
- useListKeyboard();
-}
-
-function deactivate(): void {
- keybinds.forEach((handle) => handle.unsubscribe());
-}
-
-function setupClicks(): void {
- $$("#blog .blog-list-item").forEach((item, i) => {
- item.addEventListener("click", () => {
- const id = item.getAttribute("data-id");
- if (id) {
- selectItem(i);
- openPost(id);
- setRoute("blog", id);
- }
- });
- });
-}
-
-function setupBackButton(): void {
- $(".blog-back-btn")?.addEventListener("click", () => {
- closePost();
- setRoute("blog");
- });
-}
-
-function navigate(direction: "up" | "down"): void {
- const items = $$("#blog .blog-list-item");
- const newIndex = selectedIndex + (direction === "up" ? -1 : 1);
- if (newIndex >= 0 && newIndex < items.length) {
- selectItem(newIndex);
- }
-}
-
-function selectItem(index: number): void {
- selectedIndex = index;
- setActiveByIndex("#blog .blog-list-item", index, "selected");
-
- const items = $$("#blog .blog-list-item");
- scrollIntoViewSmooth(items[index]);
-}
-
-function scrollPost(direction: string): void {
- const postView = $("#blog-post-view");
- if (postView) {
- postView.scrollBy({
- top: (direction === "up" ? -1 : 1) * SCROLL_AMOUNT,
- behavior: "smooth",
- });
- }
-}
-
-function getId(index: number): string {
- const item = $$("#blog .blog-list-item")[index];
- return item?.getAttribute("data-id") || String(index);
-}
-
-function getIndexById(id: string): number {
- const items = $$("#blog .blog-list-item");
- for (let i = 0; i < items.length; i++) {
- if (items[i].getAttribute("data-id") === id) return i;
- }
- return 0;
-}
diff --git a/src/tabs/experience.ts b/src/tabs/experience.ts
@@ -1,85 +0,0 @@
-import { $$, setActiveByIndex, scrollIntoViewSmooth } from "../ui/dom";
-import { onKey, type KeybindHandle } from "../ui/keyboard";
-import { setRoute } from "../ui/router";
-import { registerTab } from "../ui/tabs";
-
-let selectedIndex = 0;
-
-let keybinds: KeybindHandle[] = [];
-
-registerTab("experience", {
- init: () => {
- select(0);
- setupHover();
- },
-
- activate: () => {
- keybinds = [
- onKey("ArrowUp", (e) => {
- e.preventDefault();
- navigate("up");
- }),
- onKey("ArrowDown", (e) => {
- e.preventDefault();
- navigate("down");
- }),
- ];
- },
- deactivate: () => {
- keybinds.forEach((handle) => handle.unsubscribe());
- },
-
- getRouteId: () => getId(selectedIndex),
- setRouteId: (id) => {
- if (id) {
- selectedIndex = getIndexById(id);
- select(selectedIndex);
- }
- },
-});
-
-function navigate(direction: string): void {
- const cards = $$("#experience .exp-card");
- const newIndex = selectedIndex + (direction === "up" ? -1 : 1);
- if (newIndex >= 0 && newIndex < cards.length) {
- select(newIndex);
- setRoute("experience", getId(selectedIndex));
- }
-}
-
-function select(index: number): void {
- selectedIndex = index;
- setActiveByIndex("#experience .exp-card", index, "selected");
- setActiveByIndex("#experience .exp-timeline", index, "active");
-
- const rows = $$("#experience .exp-row");
- scrollIntoViewSmooth(rows[index], "start");
-}
-
-function setupHover(): void {
- $$("#experience .exp-card").forEach((card, i) => {
- card.addEventListener("mouseenter", () => {
- select(i);
- setRoute("experience", getId(selectedIndex));
- });
- });
- $$("#experience .exp-timeline").forEach((tl, i) => {
- tl.addEventListener("mouseenter", () => {
- select(i);
- setRoute("experience", getId(selectedIndex));
- });
- });
-}
-
-function getId(index: number): string {
- const item = $$("#experience .exp-card")[index];
- return item?.getAttribute("data-id") || String(index);
-}
-
-function getIndexById(id: string): number {
- const cards = $$("#experience .exp-card");
- for (let i = 0; i < cards.length; i++) {
- if (cards[i].getAttribute("data-id") === id) return i;
- }
- return 0;
-}
diff --git a/src/tabs/projects.ts b/src/tabs/projects.ts
@@ -1,88 +0,0 @@
-import { $$, setActiveByIndex, scrollIntoViewSmooth } from "../ui/dom";
-import { onKey, type KeybindHandle } from "../ui/keyboard";
-import { setRoute } from "../ui/router";
-import { registerTab } from "../ui/tabs";
-
-let selectedIndex = 0;
-
-let keybinds: KeybindHandle[] = [];
-
-registerTab("projects", {
- init: () => {
- select(0);
- setupClicks();
- },
-
- activate: () => {
- keybinds = [
- onKey("ArrowUp", (e) => {
- e.preventDefault();
- navigate("up");
- }),
- onKey("ArrowDown", (e) => {
- e.preventDefault();
- navigate("down");
- }),
- ];
- },
- deactivate: () => {
- keybinds.forEach((handle) => handle.unsubscribe());
- },
-
- getRouteId: () => getId(selectedIndex),
- setRouteId: (id) => {
- if (id) {
- selectedIndex = getIndexById(id);
- select(selectedIndex);
- }
- },
-});
-
-function navigate(direction: string): void {
- const items = $$("#projects .list-item");
- const newIndex = selectedIndex + (direction === "up" ? -1 : 1);
- if (newIndex >= 0 && newIndex < items.length) {
- select(newIndex);
- setRoute("projects", getId(selectedIndex));
- }
-}
-
-function select(index: number): void {
- selectedIndex = index;
- setActiveByIndex("#projects .list-item", index, "selected");
- updateDetail(index);
-
- const items = $$("#projects .list-item");
- scrollIntoViewSmooth(items[index]);
-}
-
-function setupClicks(): void {
- $$("#projects .list-item").forEach((item, i) => {
- item.addEventListener("click", () => {
- select(i);
- setRoute("projects", getId(i));
- });
- });
-}
-
-function updateDetail(index: number): void {
- $$("#projects .detail-content").forEach((content) => {
- content.classList.toggle(
- "hidden",
- parseInt(content.getAttribute("data-for") || "0") !== index,
- );
- });
-}
-
-function getId(index: number): string {
- const item = $$("#projects .list-item")[index];
- return item?.getAttribute("data-id") || String(index);
-}
-
-function getIndexById(id: string): number {
- const items = $$("#projects .list-item");
- for (let i = 0; i < items.length; i++) {
- if (items[i].getAttribute("data-id") === id) return i;
- }
- return 0;
-}
diff --git a/src/ui/boot.ts b/src/ui/boot.ts
@@ -1,101 +0,0 @@
-import env from "../constants/env";
-import {
- FADE_DURATION,
- GLITCH_CHARS,
- HOLD_DURATION,
- SCRAMBLE_DURATION,
- SCRAMBLE_INTERVAL,
- SUBTITLE,
- TITLE,
-} from "../constants/ui/boot";
-import { sleep } from "../utils/sleep";
-
-export async function playBootAnimation(): Promise<void> {
- const BOOT_KEY = "prime8dev-boot-shown";
- const app = document.getElementById("app");
-
- if (sessionStorage.getItem(BOOT_KEY)) {
- if (app) {
- app.classList.add("ready");
- }
- return;
- }
-
- const overlay = createBootOverlay();
-
- await runBootSequence(overlay);
-
- if (env === "prod") {
- sessionStorage.setItem(BOOT_KEY, "1");
- }
-
- if (app) {
- app.classList.add("ready", "boot-reveal");
- }
-}
-
-function createBootOverlay(): HTMLElement {
- const overlay = document.createElement("div");
- overlay.id = "boot-overlay";
- overlay.innerHTML = `
- <div class="boot-terminal">
- <div class="boot-text">
- <span class="boot-title"></span>
- <span class="boot-subtitle"></span>
- </div>
- </div>
- `;
- document.body.appendChild(overlay);
- return overlay;
-}
-
-async function runBootSequence(overlay: HTMLElement): Promise<void> {
- const title = overlay.querySelector(".boot-title") as HTMLElement;
- const subtitle = overlay.querySelector(".boot-subtitle") as HTMLElement;
-
- // Initialize with scrambled text
- title.textContent = scrambleText(TITLE.length);
- subtitle.textContent = scrambleText(SUBTITLE.length);
-
- await sleep(100);
-
- // Reveal name first, then title
- await scrambleReveal(title, TITLE, SCRAMBLE_DURATION);
- await scrambleReveal(subtitle, SUBTITLE, SCRAMBLE_DURATION);
-
- await sleep(HOLD_DURATION);
-
- overlay.classList.add("fade-out");
- await sleep(FADE_DURATION);
-
- overlay.remove();
-}
-
-function scrambleText(length: number): string {
- return Array.from({ length }, () => {
- const index = Math.floor(Math.random() * GLITCH_CHARS.length);
- return GLITCH_CHARS[index];
- }).join("");
-}
-
-async function scrambleReveal(
- element: HTMLElement,
- target: string,
- duration: number,
-): Promise<void> {
- const steps = Math.floor(duration / SCRAMBLE_INTERVAL);
- const length = target.length;
-
- for (let i = 0; i <= steps; i++) {
- const progress = i / steps;
- const revealedCount = Math.floor(progress * length);
-
- const revealed = target.slice(0, revealedCount);
- const scrambled = scrambleText(length - revealedCount);
-
- element.textContent = revealed + scrambled;
- await sleep(SCRAMBLE_INTERVAL);
- }
-
- element.textContent = target;
-}
diff --git a/src/ui/dom.ts b/src/ui/dom.ts
@@ -1,37 +0,0 @@
-export function $(
- selector: string,
- parent: Element | Document = document,
-): Element | null {
- return parent.querySelector(selector);
-}
-
-export function $$(
- selector: string,
- parent: Element | Document = document,
-): Element[] {
- return Array.from(parent.querySelectorAll(selector));
-}
-
-export function toggleClass(
- elements: Element | Element[],
- className: string,
- predicate: (el: Element, index: number) => boolean,
-): void {
- const els = elements instanceof Element ? [elements] : Array.from(elements);
- els.forEach((el, i) => el.classList.toggle(className, predicate(el, i)));
-}
-
-export function setActiveByIndex(
- selector: string,
- index: number,
- className = "selected",
-): void {
- $$(selector).forEach((el, i) => el.classList.toggle(className, i === index));
-}
-
-export function scrollIntoViewSmooth(
- element: Element | null,
- block: ScrollLogicalPosition = "nearest",
-): void {
- element?.scrollIntoView({ behavior: "smooth", block });
-}
diff --git a/src/ui/help.ts b/src/ui/help.ts
@@ -1,71 +0,0 @@
-import { onKey } from "./keyboard";
-
-interface Shortcut {
- key: string;
- description: string;
-}
-
-const shortcuts: Shortcut[] = [
- { key: "← →", description: "navigate tabs" },
- { key: "↑ ↓", description: "scroll content" },
- { key: "Tab", description: "cycle tabs" },
- { key: "1-4", description: "switch tabs" },
- { key: "Enter", description: "open item" },
- { key: "?", description: "toggle help" },
-];
-
-export function initHelp(): void {
- createHelpOverlay();
-}
-
-export function setupHelpKeybinds(): void {
- onKey("?", () => {
- toggleHelp();
- });
-}
-
-function createHelpOverlay(): void {
- const overlay = document.createElement("div");
- overlay.id = "help-overlay";
- overlay.innerHTML = `
- <div class="help-modal">
- <div class="help-header">
- <span class="help-title">Help</span>
- <span class="help-close">Press ? to close</span>
- </div>
- <div class="help-content">
- ${shortcuts
- .map(
- (s) => `
- <div class="help-row">
- <span class="help-key">${s.key}</span>
- <span class="help-desc">${s.description}</span>
- </div>
- `,
- )
- .join("")}
- </div>
- </div>
- `;
- document.body.appendChild(overlay);
-
- overlay.addEventListener("click", (e) => {
- if (e.target === overlay) {
- hideHelp();
- }
- });
-}
-
-function toggleHelp(): void {
- const overlay = document.getElementById("help-overlay");
- if (!overlay) return;
-
- overlay.classList.toggle("visible");
-}
-
-function hideHelp(): void {
- const overlay = document.getElementById("help-overlay");
- if (!overlay) return;
-
- overlay.classList.remove("visible");
-}
diff --git a/src/ui/index.ts b/src/ui/index.ts
@@ -1,27 +0,0 @@
-import { playBootAnimation } from "./boot";
-import { initRouter } from "./router";
-import { routeHandler, initTabs } from "./tabs";
-import { initKeyboard } from "./keyboard";
-import { initStatus } from "./status";
-import { initHelp } from "./help";
-
-// Register tabs (order matters)
-import "../tabs/about";
-import "../tabs/experience";
-import "../tabs/projects";
-import "../tabs/blog";
-
-export async function initUI(): Promise<void> {
- const promise = playBootAnimation();
-
- // Initialize core systems
- initKeyboard();
- initTabs();
- initRouter(routeHandler);
-
- // Initialize UI modules
- initStatus();
- initHelp();
-
- await promise;
-}
diff --git a/src/ui/keyboard.ts b/src/ui/keyboard.ts
@@ -1,37 +0,0 @@
-const handlers: Map<string, KeyHandler> = new Map();
-
-export type KeyHandler = (e: KeyboardEvent) => void;
-
-export function initKeyboard(): void {
- document.addEventListener("keydown", (e) => {
- if (shouldIgnoreKeydown(e)) return;
-
- const handler = handlers.get(e.key);
- if (handler) handler(e);
- });
-}
-
-export interface KeybindHandle {
- unsubscribe: () => void;
-}
-
-export function onKey(key: string, handler: KeyHandler): KeybindHandle {
- handlers.set(key, handler);
-
- return {
- unsubscribe: () => {
- handlers.delete(key);
- },
- };
-}
-
-export function clearAllHandlers(): void {
- handlers.clear();
-}
-
-function shouldIgnoreKeydown(e: KeyboardEvent): boolean {
- return (
- e.target instanceof HTMLInputElement ||
- e.target instanceof HTMLTextAreaElement
- );
-}
diff --git a/src/ui/router.ts b/src/ui/router.ts
@@ -1,42 +0,0 @@
-type RouteHandler = (route: { tab: string; id?: string }) => void;
-
-let currentRoute: { tab: string; id?: string } = { tab: "about" };
-
-export function initRouter(handler: RouteHandler): void {
- // Load initial route
- currentRoute = parseHash();
- handler(currentRoute);
-
- // Handle back/forward
- window.addEventListener("popstate", () => {
- currentRoute = parseHash();
- handler(currentRoute);
- });
-}
-
-export function getRoute(): { tab: string; id?: string } {
- return { ...currentRoute };
-}
-
-export function setRoute(tab: string, id?: string): void {
- currentRoute = { tab, id };
-
- const hash = id ? `#${tab}/${id}` : `#${tab}`;
- if (window.location.hash !== hash) {
- history.pushState(null, "", hash);
- }
-}
-
-function parseHash(): { tab: string; id?: string } {
- const hash = window.location.hash.slice(1);
- if (!hash) return { tab: "about" };
-
- const [tab, id] = hash.split("/");
- const validTabs = ["about", "experience", "projects", "blog"];
-
- if (validTabs.includes(tab)) {
- return { tab, id };
- }
-
- return { tab: "about" };
-}
diff --git a/src/ui/status.ts b/src/ui/status.ts
@@ -1,25 +0,0 @@
-export function initStatus(): void {
- const el = document.getElementById("status-time");
- if (!el) return;
-
- updateStatusTime();
- setInterval(updateStatusTime, 1000);
-}
-
-function updateStatusTime(): void {
- const el = document.getElementById("status-time");
- if (!el) return;
-
- const now = new Date();
- const time = now.toLocaleTimeString("en-US", {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit",
- hour12: false,
- });
- const date = now.toLocaleDateString("en-US", {
- month: "short",
- day: "numeric",
- });
- el.textContent = `${date} ${time}`;
-}
diff --git a/src/ui/tabs.ts b/src/ui/tabs.ts
@@ -1,177 +0,0 @@
-import { $$ } from "./dom";
-import { clearAllHandlers, onKey } from "./keyboard";
-import { setRoute } from "./router";
-import { setupHelpKeybinds } from "./help";
-
-const order: string[] = [];
-let currentIndex = 0;
-
-const tabs: Map<string, TabConfig> = new Map();
-
-export type TabConfig = {
- init?: () => void;
-
- activate?: () => void;
- deactivate?: () => void;
-
- getRouteId?: () => string | undefined;
- setRouteId?: (id?: string) => void;
-};
-
-let unsubscribe: (() => void) | undefined;
-
-export function initTabs(): void {
- for (const config of tabs.values()) {
- config.init?.();
- }
- setupTabClicks();
-}
-
-export function registerTab(name: string, config: TabConfig): void {
- if (tabs.has(name)) throw new Error(`Tab "${name}" already registered`);
-
- order.push(name);
- tabs.set(name, config);
-}
-
-export function routeHandler(route: { tab: string; id?: string }): void {
- const config = tabs.get(route.tab);
- if (!config) throw new Error(`Tab "${route.tab}" not found`);
-
- const index = findIndexByTabName(route.tab);
-
- deactivate();
- activate(index);
- updateDom();
-
- config.setRouteId?.(route.id);
-
- setRoute(route.tab, config.getRouteId?.());
-}
-
-export function getAllTabs(): string[] {
- return order;
-}
-
-export function getCurrentTab(): string {
- const tabName = order[currentIndex];
- if (!tabName) throw new Error(`Tab index ${currentIndex} not found`);
- return tabName;
-}
-
-export function cycleTab(direction: "prev" | "next"): void {
- const index =
- (currentIndex + (direction === "prev" ? -1 : 1) + order.length) %
- order.length;
-
- const tabName = order[index];
- if (!tabName) throw new Error(`Tab index ${index} not found`);
-
- switchTab(tabName);
-}
-
-export function switchTab(tabName: string): void {
- const index = findIndexByTabName(tabName);
-
- deactivate();
- activate(index);
- updateDom();
- updateRoute();
-}
-
-function findIndexByTabName(tabName: string): number {
- for (const [i, n] of order.entries()) {
- if (n === tabName) return i;
- }
- throw new Error(`Tab "${tabName}" not found`);
-}
-
-function activate(index: number): void {
- const name = order[index];
- if (!name) throw new Error(`Tab index ${index} not found`);
-
- const config = tabs.get(name);
- if (!config) throw new Error(`Tab "${name}" not found`);
-
- currentIndex = index;
-
- setupDefaultKeybinds();
-
- config.activate?.();
-
- unsubscribe = config.deactivate;
-}
-
-function deactivate(): void {
- if (unsubscribe) {
- unsubscribe();
- unsubscribe = undefined;
- }
- clearAllHandlers();
-}
-
-function updateDom(): void {
- const name = order[currentIndex];
- if (!name) throw new Error(`Tab index ${currentIndex} not found`);
-
- $$(".tab").forEach((tab) => {
- const isActive = tab.getAttribute("data-tab") === name;
- tab.classList.toggle("active", isActive);
- if (isActive) {
- tab.setAttribute("aria-current", "page");
- } else {
- tab.removeAttribute("aria-current");
- }
- });
-
- $$(".page").forEach((page) => {
- page.classList.toggle("active", page.id === name);
- });
-}
-
-function updateRoute(): void {
- const name = order[currentIndex];
- if (!name) throw new Error(`Tab index ${currentIndex} not found`);
-
- const config = tabs.get(name);
- if (!config) throw new Error(`Tab "${name}" not found`);
-
- setRoute(name, config.getRouteId?.());
-}
-
-function setupDefaultKeybinds(): void {
- setupHelpKeybinds();
-
- onKey("Tab", (e) => {
- e.preventDefault();
- cycleTab(e.shiftKey ? "prev" : "next");
- });
-
- for (const [i, name] of order.entries()) {
- onKey(String(i + 1), () => switchTab(name));
- }
-
- onKey("ArrowLeft", (e) => {
- if (currentIndex === 0) return;
- e.preventDefault();
- cycleTab("prev");
- });
- onKey("ArrowRight", (e) => {
- if (currentIndex === order.length - 1) return;
- e.preventDefault();
- cycleTab("next");
- });
-}
-
-function setupTabClicks(): void {
- $$(".tab").forEach((tab) => {
- tab.addEventListener("click", () => {
- const name = tab.getAttribute("data-tab");
- if (!name) {
- console.warn("Tab clicked without name");
- return;
- }
- switchTab(name);
- });
- });
-}
diff --git a/src/utils/duration.ts b/src/utils/duration.ts
@@ -1,11 +0,0 @@
-export function formatDuration(seconds: number): string {
- const hours = Math.floor(seconds / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
-
- if (hours > 0) {
- return `${hours}h ${minutes}m`;
- } else if (minutes > 0) {
- return `${minutes}m`;
- }
- return "0m";
-}
diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts
@@ -1,149 +0,0 @@
-import { marked } from "marked";
-import type { Tokens, RendererThis } from "marked";
-
-export function parseMarkdown(markdown: string): string {
- return marked.parse(markdown) as string;
-}
-
-export function removeFirstH1(markdown: string): string {
- return markdown.replace(/^#\s+.+\n+/, "");
-}
-
-const renderer = {
- heading(this: RendererThis, token: Tokens.Heading): string {
- const level = token.depth;
- const tag = `h${level + 1}`;
- const text = this.parser.parseInline(token.tokens);
- return `<${tag} class="readme-h${level}">${text}</${tag}>`;
- },
-
- code(token: Tokens.Code): string {
- const escaped = token.text.replace(/</g, "<").replace(/>/g, ">");
- return `<pre class="readme-code"><code>${escaped}</code></pre>`;
- },
-
- codespan(token: Tokens.Codespan): string {
- const escaped = token.text.replace(/</g, "<").replace(/>/g, ">");
- return `<code class="readme-inline-code">${escaped}</code>`;
- },
-
- link(this: RendererThis, token: Tokens.Link): string {
- const text = this.parser.parseInline(token.tokens);
- return `<a href="${token.href}" target="_blank" rel="noopener noreferrer" class="readme-link">${text}</a>`;
- },
-
- image(token: Tokens.Image): string {
- return `<img src="${token.href}" alt="${token.text || ""}" class="readme-img" loading="lazy" />`;
- },
-
- list(this: RendererThis, token: Tokens.List): string {
- function renderList(
- parser: RendererThis["parser"],
- token: Tokens.List,
- ): string {
- const tag = token.ordered ? "ol" : "ul";
- const items = token.items
- .map((item) => {
- let content = "";
- for (const tok of item.tokens) {
- if (tok.type === "paragraph" && "tokens" in tok && tok.tokens) {
- content += parser.parseInline(tok.tokens as Tokens.Generic[]);
- } else if (tok.type === "text") {
- const textToken = tok as Tokens.Text;
- if (textToken.tokens && textToken.tokens.length > 0) {
- content += parser.parseInline(textToken.tokens);
- } else {
- const inlineTokens = marked.lexer(
- textToken.raw || textToken.text,
- { gfm: true },
- );
- if (
- inlineTokens.length > 0 &&
- inlineTokens[0].type === "paragraph" &&
- "tokens" in inlineTokens[0]
- ) {
- content += parser.parseInline(
- (inlineTokens[0] as Tokens.Paragraph).tokens,
- );
- } else {
- content += textToken.text || "";
- }
- }
- } else if (tok.type === "list") {
- content += renderList(parser, tok as Tokens.List);
- } else {
- content += parser.parse([tok as Tokens.Generic]);
- }
- }
- return `<li>${content}</li>`;
- })
- .join("");
- return `<${tag} class="readme-list">${items}</${tag}>`;
- }
-
- return renderList(this.parser, token);
- },
-
- paragraph(this: RendererThis, token: Tokens.Paragraph): string {
- const text = this.parser.parseInline(token.tokens);
- return `<p class="readme-p">${text}</p>`;
- },
-
- table(this: RendererThis, token: Tokens.Table): string {
- const headerCells = token.header
- .map((h) => {
- const text = this.parser.parseInline(h.tokens as Tokens.Generic[]);
- return `<th>${text}</th>`;
- })
- .join("");
- const bodyRows = token.rows
- .map((row) => {
- const cells = row
- .map((cell) => {
- const text = this.parser.parseInline(
- cell.tokens as Tokens.Generic[],
- );
- return `<td>${text}</td>`;
- })
- .join("");
- return `<tr>${cells}</tr>`;
- })
- .join("");
- return `<table class="readme-table"><thead><tr>${headerCells}</tr></thead><tbody>${bodyRows}</tbody></table>`;
- },
-
- blockquote(this: RendererThis, token: Tokens.Blockquote): string {
- const text = this.parser.parse(token.tokens);
- return `<blockquote class="readme-blockquote">${text}</blockquote>`;
- },
-
- hr(): string {
- return "<hr />";
- },
-
- strong(this: RendererThis, token: Tokens.Strong): string {
- const text = this.parser.parseInline(token.tokens);
- return `<strong>${text}</strong>`;
- },
-
- em(this: RendererThis, token: Tokens.Em): string {
- const text = this.parser.parseInline(token.tokens);
- return `<em>${text}</em>`;
- },
-
- del(this: RendererThis, token: Tokens.Del): string {
- const text = this.parser.parseInline(token.tokens);
- return `<del>${text}</del>`;
- },
-
- text(token: Tokens.Text | Tokens.Escape): string {
- return token.text;
- },
-};
-
-marked.use({ renderer });
-
-marked.setOptions({
- gfm: true,
- breaks: true,
-});
diff --git a/src/utils/period.ts b/src/utils/period.ts
@@ -1,27 +0,0 @@
-import { MONTH_NAMES } from "../constants/calendar";
-
-export function formatPeriod(startDate: string, endDate?: string): string {
- const months = MONTH_NAMES.map((m) => m.slice(0, 3));
-
- const [startYear, startMonth] = startDate.split("-").map(Number);
- const startStr = `${months[startMonth - 1]} ${startYear}`;
-
- if (!endDate) {
- const totalMonths = calculateMonths(startDate);
- return `${startStr} – Present (${totalMonths} mos)`;
- }
-
- const [endYear, endMonth] = endDate.split("-").map(Number);
- const endStr = `${months[endMonth - 1]} ${endYear}`;
- const totalMonths = calculateMonths(startDate, endDate);
- return `${startStr} – ${endStr} (${totalMonths} mos)`;
-}
-
-export function calculateMonths(startDate: string, endDate?: string): number {
- const [startYear, startMonth] = startDate.split("-").map(Number);
- const end = endDate
- ? endDate.split("-").map(Number)
- : [new Date().getFullYear(), new Date().getMonth() + 1];
- const [endYear, endMonth] = end;
- return (endYear - startYear) * 12 + (endMonth - startMonth) + 1;
-}
diff --git a/src/utils/sleep.ts b/src/utils/sleep.ts
@@ -1,3 +0,0 @@
-export function sleep(ms: number): Promise<void> {
- return new Promise((resolve) => setTimeout(resolve, ms));
-}
diff --git a/src/utils/tags.ts b/src/utils/tags.ts
@@ -1,16 +0,0 @@
-import { TAG_CLASS_MAP, TECH_ABBREVIATIONS } from "../constants/ui/tags";
-
-export function getTagClass(tag: string): string {
- const className = TAG_CLASS_MAP[tag];
- if (!className) {
- throw new Error(`Tag ${tag} not found`);
- }
- return className;
-}
-
-export function getTechMeta(tech: string[]): string {
- return tech
- .slice(0, 2)
- .map((t) => TECH_ABBREVIATIONS[t] || t)
- .join(", ");
-}
diff --git a/public/amplifier/logo.svg b/static/amplifier/logo.svg
diff --git a/public/favicon.png b/static/favicon.png
Binary files differ.
diff --git a/static/hark/logo.svg b/static/hark/logo.svg
@@ -0,0 +1,3 @@
+<svg width="1019" height="343" viewBox="0 0 1019 343" fill="none" xmlns="http://www.w3.org/2000/svg" data-component="wordmark" aria-hidden="true">
+ <path d="M243.946 280.197V172.466C243.946 124.64 217.378 91.7888 162.31 91.7886C125.597 91.7886 102.409 104.349 89.3661 122.224V0H0V24.1542H2.41553C26.0853 24.1543 36.2301 34.3 36.2302 57.9714V280.197C36.2302 303.869 26.0854 314.014 2.41553 314.014H0V338.169H125.596V314.014H123.181C99.5108 314.014 89.3661 303.869 89.3661 280.197V176.331C89.3661 144.447 105.791 128.988 140.088 128.988C174.386 128.988 190.81 143.481 190.81 176.331V280.197C190.81 303.869 180.665 314.014 156.995 314.014H154.58V338.169H280.176V314.014H277.761C254.091 314.014 243.946 303.869 243.946 280.197ZM498.644 280.197V169.084C498.644 114.977 457.583 91.7886 405.413 91.7886C341.648 91.7886 304.233 124.64 304.233 160.389C304.233 176.814 315.826 187.925 331.284 187.925C346.742 187.925 358.818 176.814 358.818 157.973C358.818 133.336 371.598 118.359 400.582 118.359C429.082 118.359 445.506 132.369 445.506 157.973V175.848L349.157 214.979C313.411 228.988 299.885 252.661 299.885 280.68C299.885 316.43 327.419 343 375.242 343C406.641 343 430.048 333.821 445.506 313.531V338.169H534.874V314.014H532.458C508.788 314.014 498.644 303.869 498.644 280.197ZM445.506 262.805C445.506 293.241 420.168 308.216 396.014 308.217C370.412 308.217 353.022 292.758 353.022 269.086C353.022 248.313 366.064 234.303 391.183 224.641L445.506 201.935V262.805ZM828.19 280.197V0H738.824V24.1542H741.238C764.908 24.1542 775.052 34.2999 775.052 57.9714V280.197C775.052 303.869 764.908 314.014 741.238 314.014H738.824V338.169H864.419V314.014H862.005C838.335 314.014 828.19 303.869 828.19 280.197ZM711.292 91.7886C696.318 91.7887 685.339 99.0353 674.711 108.214L644.279 135.75V96.62H554.912V120.774H557.326C580.996 120.774 591.141 130.92 591.141 154.591V280.197C591.141 303.869 580.997 314.014 557.326 314.014H554.912V338.169H689.686V314.014H678.093C654.423 314.014 644.279 303.869 644.279 280.197V181.161C644.279 160.389 657.322 147.346 678.093 147.346H710.326C728.682 147.346 741.725 135.75 741.725 119.325C741.725 102.9 729.648 91.7886 711.292 91.7886ZM1016.58 314.014C992.915 314.014 978.422 305.319 964.413 285.995L903.547 203.868C885.674 179.713 899.683 147.346 931.565 147.346H959.099C977.455 147.346 990.498 135.75 990.498 119.325C990.498 102.9 978.421 91.7886 960.065 91.7886C937.845 91.7887 922.387 103.866 903.064 124.639L834.469 199.036L936.879 338.169H1019V314.014H1016.58Z" fill="currentcolor"/>
+</svg>
diff --git a/public/mandolin/company.jpeg b/static/mandolin/company.jpeg
Binary files differ.
diff --git a/public/mandolin/company_silly.jpeg b/static/mandolin/company_silly.jpeg
Binary files differ.
diff --git a/public/mandolin/employee_polaroids.jpeg b/static/mandolin/employee_polaroids.jpeg
Binary files differ.
diff --git a/public/mandolin/logo.svg b/static/mandolin/logo.svg
diff --git a/public/mandolin/moment_polaroids.jpeg b/static/mandolin/moment_polaroids.jpeg
Binary files differ.
diff --git a/public/me.jpg b/static/me.jpg
Binary files differ.
diff --git a/public/robots.txt b/static/robots.txt
diff --git a/public/site.webmanifest b/static/site.webmanifest
diff --git a/static/style.css b/static/style.css
@@ -0,0 +1,369 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --bg: #0d0d0d;
+ --bg-alt: #161616;
+ --fg: #c0c0c0;
+ --fg-dim: #606060;
+ --accent: #d7af5f;
+ --accent-dim: #a68a3d;
+ --cyan: #5fafaf;
+ --blue: #5f87d7;
+ --light-blue: #5fafff;
+ --yellow: #d7af5f;
+ --magenta: #af5faf;
+ --red: #d75f5f;
+ --border: #333;
+ --selection: #3d3520;
+}
+
+@font-face {
+ font-family: "JetBrains Mono";
+ src: url("https://cdn.jsdelivr.net/npm/@fontsource/jetbrains-mono@5.0.18/files/jetbrains-mono-latin-400-normal.woff2")
+ format("woff2");
+ font-weight: 400;
+}
+
+body {
+ background: var(--bg);
+ color: var(--fg);
+ font-family: "JetBrains Mono", "SF Mono", "Fira Code", monospace;
+ font-size: 14px;
+ line-height: 1.5;
+ min-height: 100vh;
+ overflow: hidden;
+}
+
+::-webkit-scrollbar {
+ width: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--bg);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--fg-dim);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--fg);
+}
+
+a {
+ color: var(--light-blue);
+ text-decoration: none;
+}
+
+a:hover {
+ opacity: 0.8;
+}
+
+#app {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ opacity: 0;
+}
+
+#app.ready {
+ opacity: 1;
+}
+
+#content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 24px;
+}
+
+/* Header / Tabs */
+#tabs {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ background: var(--bg-alt);
+ border-bottom: 1px solid var(--border);
+ padding: 0 12px;
+ height: 36px;
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ user-select: none;
+}
+
+#tabs * {
+ user-select: none;
+}
+
+.tab-nav {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.tab {
+ padding: 6px 12px;
+ color: var(--fg-dim);
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition: color 0.15s, border-color 0.15s;
+}
+
+.tab:hover {
+ color: var(--fg);
+}
+
+.tab.active {
+ color: var(--accent);
+ border-bottom-color: var(--accent);
+}
+
+.tab-spacer {
+ flex: 1;
+}
+
+.status {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--fg-dim);
+ font-size: 11px;
+ margin-right: 8px;
+}
+
+.dpad {
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1px;
+ font-size: 8px;
+ line-height: 1;
+}
+
+.dpad-row {
+ display: flex;
+ gap: 1px;
+}
+
+.dpad-key {
+ width: 12px;
+ height: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--border);
+ border-radius: 2px;
+ color: var(--fg);
+}
+
+/* Footer / Status Bar */
+#statusbar {
+ display: flex;
+ align-items: center;
+ background: var(--bg-alt);
+ border-top: 1px solid var(--border);
+ padding: 0 12px;
+ height: 24px;
+ font-size: 12px;
+ position: sticky;
+ bottom: 0;
+ z-index: 100;
+}
+
+#statusbar .time {
+ color: var(--cyan);
+}
+
+#statusbar .spacer {
+ flex: 1;
+}
+
+#statusbar .wakatime {
+ color: var(--magenta);
+ text-decoration: none;
+}
+
+#statusbar .wakatime:hover {
+ opacity: 0.8;
+}
+
+/* Boot Animation */
+#boot-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: var(--bg);
+ z-index: 9999;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+#boot-overlay.fade-out {
+ animation: bootFadeOut 0.3s ease-out forwards;
+}
+
+.boot-terminal {
+ text-align: center;
+}
+
+.boot-text {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.boot-title {
+ font-family: "JetBrains Mono", "SF Mono", monospace;
+ font-size: 32px;
+ font-weight: 600;
+ color: var(--accent);
+ letter-spacing: -0.5px;
+}
+
+.boot-subtitle {
+ font-family: "JetBrains Mono", "SF Mono", monospace;
+ font-size: 14px;
+ color: var(--fg-dim);
+ letter-spacing: 0.5px;
+}
+
+@keyframes bootFadeOut {
+ 0% { opacity: 1; }
+ 100% { opacity: 0; }
+}
+
+#app.boot-reveal {
+ animation: revealContent 0.5s ease-out;
+}
+
+@keyframes revealContent {
+ 0% { opacity: 0; transform: translateY(15px); }
+ 100% { opacity: 1; transform: translateY(0); }
+}
+
+/* Help Overlay */
+#help-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(0, 0, 0, 0.8);
+ z-index: 9998;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.2s, visibility 0.2s;
+}
+
+#help-overlay.visible {
+ opacity: 1;
+ visibility: visible;
+}
+
+.help-modal {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ padding: 20px 24px;
+ min-width: 280px;
+ max-width: 90vw;
+}
+
+.help-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid var(--border);
+}
+
+.help-title {
+ color: var(--accent);
+ font-weight: 600;
+}
+
+.help-close {
+ color: var(--fg-dim);
+ font-size: 11px;
+}
+
+.help-content {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.help-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 24px;
+}
+
+.help-key {
+ color: var(--cyan);
+ font-family: "JetBrains Mono", monospace;
+ font-size: 12px;
+ min-width: 60px;
+}
+
+.help-desc {
+ color: var(--fg);
+ font-size: 13px;
+}
+
+/* Mobile */
+@media (max-width: 768px) {
+ body {
+ font-size: 13px;
+ overflow-y: auto;
+ }
+
+ #app {
+ min-height: 100vh;
+ height: auto;
+ }
+
+ #tabs {
+ flex-wrap: wrap;
+ height: auto;
+ min-height: 36px;
+ padding: 8px 12px;
+ gap: 8px;
+ }
+
+ .tab-nav {
+ width: 100%;
+ justify-content: space-between;
+ gap: 0;
+ }
+
+ .tab {
+ padding: 6px 8px;
+ font-size: 12px;
+ flex: 1;
+ text-align: center;
+ }
+
+ .tab-spacer,
+ .status {
+ display: none;
+ }
+
+ .help-modal {
+ padding: 16px 20px;
+ min-width: 260px;
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
@@ -1,6 +0,0 @@
-{
- "extends": "astro/tsconfigs/strict",
- "compilerOptions": {
- "strictNullChecks": true
- }
-}