prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

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:
A.air.toml | 18++++++++++++++++++
M.gitignore | 29+++++------------------------
M.pre-commit-config.yaml | 12+++++++++---
D.prettierignore | 3---
D.prettierrc | 16----------------
MDockerfile | 29+++++++++++++++--------------
MREADME.md | 26+++++++++++++-------------
Dastro.config.ts | 18------------------
Dbun.lock | 1264-------------------------------------------------------------------------------
Acmd/buildjs/main.go | 29+++++++++++++++++++++++++++++
Acmd/server/main.go | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acontent/blogs/building-my-portfolio.md | 42++++++++++++++++++++++++++++++++++++++++++
Menv.example | 4++--
Deslint.config.js | 24------------------------
Ago.mod | 18++++++++++++++++++
Ago.sum | 18++++++++++++++++++
Ainternal/config/about.go | 31+++++++++++++++++++++++++++++++
Ainternal/config/experience.go | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/config/projects.go | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/config/site.go | 6++++++
Ainternal/content/blogs.go | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/external/gitlab.go | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/external/wakatime.go | 282+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/handlers/blog.go | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/handlers/cache.go | 6++++++
Ainternal/handlers/experience.go | 46++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/handlers/handler.go | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/handlers/home.go | 24++++++++++++++++++++++++
Ainternal/handlers/projects.go | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/handlers/render.go | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/handlers/sitemap.go | 42++++++++++++++++++++++++++++++++++++++++++
Ainternal/handlers/wakatime.go | 24++++++++++++++++++++++++
Ainternal/utils/htmx.go | 7+++++++
Ainternal/utils/markdown.go | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/utils/period.go | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/utils/tab.go | 19+++++++++++++++++++
Ainternal/utils/tags.go | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/utils/url.go | 7+++++++
Ainternal/view/ascii.go | 34++++++++++++++++++++++++++++++++++
Ainternal/view/renderer.go | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/scripts/app.js | 46++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/scripts/modules/about.js | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/scripts/modules/blog.js | 183+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/scripts/modules/blogDetail.js | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/scripts/modules/dom.js | 8++++++++
Ainternal/view/scripts/modules/experience.js | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/scripts/modules/header.js | 212+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/scripts/modules/help.js | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/scripts/modules/layout.js | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/scripts/modules/lightbox.js | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/scripts/modules/projects.js | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/scripts/modules/wakatime.js | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/styles/about.css | 276+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/styles/blog.css | 197+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/styles/blog_detail.css | 149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/styles/experience.css | 326+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/styles/footer.css | 29+++++++++++++++++++++++++++++
Ainternal/view/styles/header.css | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/styles/layout.css | 440+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/styles/lightbox.css | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/styles/project_detail.css | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/styles/projects.css | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/templates/layout.gohtml | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/templates/partials/about.gohtml | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/templates/partials/blog.gohtml | 46++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/templates/partials/blog_detail.gohtml | 28++++++++++++++++++++++++++++
Ainternal/view/templates/partials/experience.gohtml | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/view/templates/partials/footer.gohtml | 9+++++++++
Ainternal/view/templates/partials/header.gohtml | 21+++++++++++++++++++++
Ainternal/view/templates/partials/lightbox.gohtml | 11+++++++++++
Ainternal/view/templates/partials/project_detail.gohtml | 18++++++++++++++++++
Ainternal/view/templates/partials/projects.gohtml | 28++++++++++++++++++++++++++++
Dpackage.json | 40----------------------------------------
Dsrc/blogs/building-my-portfolio.md | 42------------------------------------------
Dsrc/blogs/index.ts | 56--------------------------------------------------------
Dsrc/config/about.ts | 32--------------------------------
Dsrc/config/contact.ts | 11-----------
Dsrc/config/experience.ts | 60------------------------------------------------------------
Dsrc/config/index.ts | 4----
Dsrc/config/projects.ts | 44--------------------------------------------
Dsrc/config/tabs.ts | 8--------
Dsrc/constants/calendar.ts | 24------------------------
Dsrc/constants/env.ts | 1-
Dsrc/constants/external/wakatime.ts | 3---
Dsrc/constants/ui/boot.ts | 11-----------
Dsrc/constants/ui/scroll.ts | 1-
Dsrc/constants/ui/tags.ts | 34----------------------------------
Dsrc/constants/website.ts | 3---
Dsrc/external/git.ts | 85-------------------------------------------------------------------------------
Dsrc/external/wakatime.ts | 159-------------------------------------------------------------------------------
Dsrc/layouts/Layout.astro | 547-------------------------------------------------------------------------------
Dsrc/layouts/components/Article.astro | 387-------------------------------------------------------------------------------
Dsrc/layouts/components/Footer.astro | 60------------------------------------------------------------
Dsrc/layouts/components/Header.astro | 156-------------------------------------------------------------------------------
Dsrc/layouts/components/Lightbox.astro | 135-------------------------------------------------------------------------------
Dsrc/layouts/tabs/About.astro | 430-------------------------------------------------------------------------------
Dsrc/layouts/tabs/Blog.astro | 230-------------------------------------------------------------------------------
Dsrc/layouts/tabs/Experience.astro | 488-------------------------------------------------------------------------------
Dsrc/layouts/tabs/Projects.astro | 411-------------------------------------------------------------------------------
Dsrc/pages/404.astro | 83-------------------------------------------------------------------------------
Dsrc/pages/blog/[slug].astro | 118-------------------------------------------------------------------------------
Dsrc/pages/experience/[id].astro | 376-------------------------------------------------------------------------------
Dsrc/pages/index.astro | 58----------------------------------------------------------
Dsrc/pages/projects/[id].astro | 296-------------------------------------------------------------------------------
Dsrc/pages/sitemap.xml.ts | 76----------------------------------------------------------------------------
Dsrc/schemas/blog.ts | 60------------------------------------------------------------
Dsrc/schemas/experience.ts | 54------------------------------------------------------
Dsrc/schemas/index.ts | 13-------------
Dsrc/schemas/person.ts | 22----------------------
Dsrc/schemas/project.ts | 46----------------------------------------------
Dsrc/schemas/website.ts | 12------------
Dsrc/tabs/about.ts | 3---
Dsrc/tabs/blog.ts | 191-------------------------------------------------------------------------------
Dsrc/tabs/experience.ts | 85-------------------------------------------------------------------------------
Dsrc/tabs/projects.ts | 88-------------------------------------------------------------------------------
Dsrc/ui/boot.ts | 101-------------------------------------------------------------------------------
Dsrc/ui/dom.ts | 37-------------------------------------
Dsrc/ui/help.ts | 71-----------------------------------------------------------------------
Dsrc/ui/index.ts | 27---------------------------
Dsrc/ui/keyboard.ts | 37-------------------------------------
Dsrc/ui/router.ts | 42------------------------------------------
Dsrc/ui/status.ts | 25-------------------------
Dsrc/ui/tabs.ts | 177-------------------------------------------------------------------------------
Dsrc/utils/duration.ts | 11-----------
Dsrc/utils/markdown.ts | 149-------------------------------------------------------------------------------
Dsrc/utils/period.ts | 27---------------------------
Dsrc/utils/sleep.ts | 3---
Dsrc/utils/tags.ts | 16----------------
Rpublic/amplifier/logo.svg -> static/amplifier/logo.svg | 0
Rpublic/favicon.png -> static/favicon.png | 0
Astatic/hark/logo.svg | 3+++
Rpublic/mandolin/company.jpeg -> static/mandolin/company.jpeg | 0
Rpublic/mandolin/company_silly.jpeg -> static/mandolin/company_silly.jpeg | 0
Rpublic/mandolin/employee_polaroids.jpeg -> static/mandolin/employee_polaroids.jpeg | 0
Rpublic/mandolin/logo.svg -> static/mandolin/logo.svg | 0
Rpublic/mandolin/moment_polaroids.jpeg -> static/mandolin/moment_polaroids.jpeg | 0
Rpublic/me.jpg -> static/me.jpg | 0
Rpublic/robots.txt -> static/robots.txt | 0
Rpublic/site.webmanifest -> static/site.webmanifest | 0
Astatic/style.css | 369+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtsconfig.json | 6------
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 &copy, 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("![%s](%s/%s)", 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, - `![$1](${raw}/$2)`, - ); - - 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, "&lt;").replace(/>/g, "&gt;"); - return `<pre class="readme-code"><code>${escaped}</code></pre>`; - }, - - codespan(token: Tokens.Codespan): string { - const escaped = token.text.replace(/</g, "&lt;").replace(/>/g, "&gt;"); - 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 - } -}