diff --git a/.gitea/workflows/workflow.yaml b/.gitea/workflows/workflow.yaml index 68d7efc..b96addc 100644 --- a/.gitea/workflows/workflow.yaml +++ b/.gitea/workflows/workflow.yaml @@ -16,21 +16,23 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 + # Astro needs Node 22.23 or newer; runner images can cache an older 22.x. + check-latest: true - name: Install dependencies - run: npm install --legacy-peer-deps + run: npm ci - - name: Build Gatsby site - run: npx gatsby build + - name: Build Astro site + run: npm run build env: NODE_ENV: production CI: true - name: Check build output run: | - if [ ! -d "public" ]; then - echo "Build failed - public directory not found" + if [ ! -f "dist/index.html" ]; then + echo "Build failed - dist/index.html not found" exit 1 fi @@ -55,7 +57,7 @@ jobs: --exclude='mailadmin/' \ --exclude='postfixadmin/' \ --exclude='roundcube/' \ - public/ \ + dist/ \ $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH - name: Verify deployment diff --git a/.gitignore b/.gitignore index b57cf42..2be57b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ # Project dependencies -.cache node_modules yarn-error.log -package-lock.json yarn.lock -# Build directory -/public +# Build output and Astro's generated files +/dist +.astro .DS_Store + +# Specifics that must never reach the site (see test/private-banned.mjs) +test/banned.private.json diff --git a/.nvmrc b/.nvmrc index 209e3ef..2bd5a0a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 +22 diff --git a/README.md b/README.md index 6d70f14..a27eea9 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,58 @@ Personal site of **Dennis Thiessen** — Staff Data, Analytics & AI Engineer (Bern, Switzerland). -Built with [Gatsby](https://www.gatsbyjs.com/) 5 and React 18. Single-page, Swiss-minimal design; styles are plain CSS in `src/assets/style.css` (no preprocessor). +Built with [Astro](https://astro.build/) and one React island. The home page is a dark, terminal-style +agent that answers questions about Dennis's work, stack and projects, with a plain page one click away +("Classic view") and an optional light theme. Styles are plain CSS in `src/assets/style.css` +(no preprocessor). Fonts are self-hosted, so visitors make no request to any third party, and the site +uses no analytics. + +The content is deliberately focused on what recruiters scan for: current role, headline stack, +what was delivered at each employer and at what scope, education, and contact. Personal, private and +employer-internal details are left out on purpose. `npm test` checks every answer the terminal can give +against a list of things that must never be published; the specifics live in `test/banned.private.json`, +which is git-ignored so the list itself never leaks. + +## How the agent works + +There is no language model and no network call. Every answer is generated in the browser from one +curated knowledge base, so nothing can be invented. + +| Path | Role | +| ---------------------- | ------------------------------------------------------------------------------ | +| `src/data/profile.mjs` | The only place facts live: roles, skills (by how they were used), projects | +| `src/agent/engine.mjs` | Command parser, intent matcher and answer builder. Pure functions, no DOM | +| `src/agent/oncall.mjs` | The short `/oncall` choose-your-path scene | +| `src/components/` | `Terminal` (input, typing effect), `blocks` (answer renderers), `Classic`, `Shell` | +| `src/layouts/Base.astro` | Document shell: fonts, icons, theme script | +| `src/pages/` | `index.astro` (head tags, share card, JSON-LD) and `404.astro` | +| `public/` | Static files copied as-is: share card, icons, web manifest | +| `test/` | Routing tests and a content guard (see below) | + +To change what the site says, edit `src/data/profile.mjs`. Skills carry a tier (`now`, `before`, `ai`, +`poc`, `study`); a skill that is not listed is answered with "not in his listed stack" via `NON_CLAIMS`. + +Deep links work: `thiessen.io/#/projects`, `#/experience bosch`, `#classic`. ## Develop -```bash -npm install --legacy-peer-deps -npx gatsby develop # http://localhost:8000 -``` - -## Build +Needs Node 22.23 or newer (see `.nvmrc`). ```bash -npx gatsby build # output in ./public +npm install +npm run dev # http://localhost:4321 +npm test # node's built-in test runner, no extra dependencies +npm run build # static site in ./dist +npm run preview # serve ./dist locally ``` -Use Node 20 (see `.nvmrc`). +After changing `public/icons/icon-512.png`, regenerate the favicon, touch icon and manifest with +`node scripts/make-icons.mjs`. ## Deploy Pushing to `master` triggers the Gitea Actions workflow in -`.gitea/workflows/workflow.yaml`, which builds the site and rsyncs `public/` +`.gitea/workflows/workflow.yaml`, which builds the site and rsyncs `dist/` to the web server over SSH. It relies on these repository secrets: `SSH_PRIVATE_KEY`, `SSH_KNOWN_HOSTS`, `DEPLOY_USER`, `DEPLOY_HOST`, `DEPLOY_PATH`. +The rsync does not delete files that are no longer part of the site. diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..f4e8290 --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,10 @@ +import react from '@astrojs/react' +import { defineConfig } from 'astro/config' + +// A static site: `astro build` writes plain HTML, CSS and JS to ./dist, which the +// deploy workflow rsyncs to the web server. +export default defineConfig({ + site: 'https://thiessen.io', + integrations: [react()], + devToolbar: { enabled: false }, +}) diff --git a/gatsby-browser.js b/gatsby-browser.js deleted file mode 100644 index b1e5c31..0000000 --- a/gatsby-browser.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Implement Gatsby's Browser APIs in this file. - * - * See: https://www.gatsbyjs.org/docs/browser-apis/ - */ - -// You can delete this file if you're not using it diff --git a/gatsby-config.js b/gatsby-config.js deleted file mode 100644 index 9b883d6..0000000 --- a/gatsby-config.js +++ /dev/null @@ -1,24 +0,0 @@ -module.exports = { - siteMetadata: { - title: "Dennis Thiessen — Data, Analytics & AI Engineer", - author: "Dennis Thiessen", - description: - "Dennis Thiessen is a Staff Data, Analytics & AI Engineer in Bern, Switzerland, building the data pipelines and ML infrastructure that production systems run on." - }, - plugins: [ - 'gatsby-plugin-react-helmet', - { - resolve: `gatsby-plugin-manifest`, - options: { - name: 'Dennis Thiessen', - short_name: 'D. Thiessen', - start_url: '/', - background_color: '#f4f5f7', - theme_color: '#15171c', - display: 'minimal-ui', - icon: 'src/assets/images/website-icon-dt.png', - }, - }, - 'gatsby-plugin-remove-serviceworker' - ], -} diff --git a/gatsby-node.js b/gatsby-node.js deleted file mode 100644 index 2f42665..0000000 --- a/gatsby-node.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Implement Gatsby's Node APIs in this file. - * - * See: https://www.gatsbyjs.org/docs/node-apis/ - */ - -// You can delete this file if you're not using it diff --git a/gatsby-ssr.js b/gatsby-ssr.js deleted file mode 100644 index b17b8fc..0000000 --- a/gatsby-ssr.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Implement Gatsby's SSR (Server Side Rendering) APIs in this file. - * - * See: https://www.gatsbyjs.org/docs/ssr-apis/ - */ - -// You can delete this file if you're not using it diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..464f009 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3826 @@ +{ + "name": "thiessen-io", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "thiessen-io", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@astrojs/react": "^7.1.1", + "@fontsource-variable/hanken-grotesk": "^5.3.0", + "@fontsource-variable/jetbrains-mono": "^5.3.0", + "@fontsource-variable/space-grotesk": "^5.3.0", + "astro": "^7.4.2", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "prettier": "^3.1.0" + }, + "engines": { + "node": ">=22.23.4" + } + }, + "node_modules/@astrojs/compiler-binding": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.5.2.tgz", + "integrity": "sha512-lEn76+0JcMQ5RDXU0wSMWJg2ZppiZvSULpghC29cfv4o9bhvWFDbQjsCtvjjXN9iVLFQ7fL3wTR2L2UU13mP8g==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@astrojs/compiler-binding-android-arm64": "0.5.2", + "@astrojs/compiler-binding-darwin-arm64": "0.5.2", + "@astrojs/compiler-binding-darwin-x64": "0.5.2", + "@astrojs/compiler-binding-linux-arm64-gnu": "0.5.2", + "@astrojs/compiler-binding-linux-arm64-musl": "0.5.2", + "@astrojs/compiler-binding-linux-x64-gnu": "0.5.2", + "@astrojs/compiler-binding-linux-x64-musl": "0.5.2", + "@astrojs/compiler-binding-wasm32-wasi": "0.5.2", + "@astrojs/compiler-binding-win32-arm64-msvc": "0.5.2", + "@astrojs/compiler-binding-win32-x64-msvc": "0.5.2" + } + }, + "node_modules/@astrojs/compiler-binding-android-arm64": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-android-arm64/-/compiler-binding-android-arm64-0.5.2.tgz", + "integrity": "sha512-kckIxon6Rm/Bxzh9mGPYpRRqUbToUQoHptCmpMmE+Yt0Xumq8CqrKUidyYhrBsrHVO7bxuRzzWUUqvpf+iYceQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-darwin-arm64": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.5.2.tgz", + "integrity": "sha512-1ZG5Ar3dD8HtodX9XH1mRmE0qBXZlvjYXSFfpgFwocqfWnvhFuqtGUNseL8wgfImiRMztIjpQqlogQFQQtgiag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-darwin-x64": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.5.2.tgz", + "integrity": "sha512-+kl35S7hiHJ2cb7eujwEsPFYYuSgE11uEE2Z20lAJ+undpgulMcW+weQqFPOSSZccEKHOiLIY+kQHkb4e4092w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-arm64-gnu": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.5.2.tgz", + "integrity": "sha512-BqlR0EHNQLPLwgL9+xogjsAc7zGf0cHho2CPdhv0P1yqbVMwGVaGAqrANP6VYAXCqSC43AF4Czcxc2n+oqdesg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-arm64-musl": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.5.2.tgz", + "integrity": "sha512-GRGyby3AZWw5d6JQzidyxPyvGgHWsSusrpgKyFVpDTKctABF7L3Pd5V1cRJbV36/rcu+Eh292/wL7pF7MkmGkQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-x64-gnu": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.5.2.tgz", + "integrity": "sha512-9XAqGTMQAtIslMsjK3xdyY6OI9d59OZqcLBB6pvD/DcoR0daYswfrKx2azdDERDY4AqAnWPC7odVM1BmxYv3Vw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-x64-musl": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.5.2.tgz", + "integrity": "sha512-jOViewGeTh9bSfS8vK72ECxoy2yvmA+Pd/ZcEvBI5c7+7IcfHwZ5qA4L9fRYuOhSrAifvQ2dV4vm1XAtloaWBw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-wasm32-wasi": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.5.2.tgz", + "integrity": "sha512-Z7p64doar1uvR5C9Xs6j1CV9lGlPVQX7OQ2aAtZ/xKC5aGS6GB13nyc2UfQXTQLvZrvVwsyaflTQy6ziY5Bekw==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.3", + "@emnapi/runtime": "1.11.3", + "@napi-rs/wasm-runtime": "^1.2.5" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-win32-arm64-msvc": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.5.2.tgz", + "integrity": "sha512-Tr13bh+ahTWsjXjzctK7pFpWV+dUU4FtBfETlR7gO+ueex0F+RDch7eRr0YfKysaRhk9tWm14lsCJ5uWyFPNHQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-win32-x64-msvc": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.5.2.tgz", + "integrity": "sha512-SfKkI+jOQYLh6wojPvVPV0lFfQnxisweN9B+qCzRvZp7DHAuGif8Y0WcC5PUgN4jb8VKy57LggQDtggR46Du1g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-rs": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.5.2.tgz", + "integrity": "sha512-IvwYLB8t1M86rKO4oPdBi9GP4gzXcgrVlNgocjwos1jeeUuEIV8d9Crn1PvYD/5IKteWOxpIHomf7e8iYlkE+Q==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler-binding": "0.5.2" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.13.0.tgz", + "integrity": "sha512-20jxQqnSmpskcGyad7N5l6Xf1gz0UCa6lNde44bEBdaXGpncEYfAZJ142r2ZgzBaG4dUyzGmhqZWv1INU2eiHQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.5", + "@types/mdast": "^4.0.4", + "picomatch": "^4.0.7", + "retext-smartypants": "^6.2.0", + "shiki": "^4.5.0", + "smol-toml": "^1.9.1", + "unified": "^11.0.5", + "yaml": "^2.9.1" + } + }, + "node_modules/@astrojs/markdown-satteri": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.4.5.tgz", + "integrity": "sha512-0IeGdoFbv0f+442KJhYS44Wg8F3MuHmRsxIEYuFyqkjRubc6dPBFZ7azor5t4AeXLQoT5A5h9HyViKpd70Dl9w==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.13.0", + "@astrojs/prism": "4.1.1", + "github-slugger": "^2.0.0", + "satteri": "^0.10.5" + } + }, + "node_modules/@astrojs/prism": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.1.1.tgz", + "integrity": "sha512-sXlAXJX6t4QfuLg+CuCDzpg7NoxWcyb4paFprgtFppLeisCBmh935h+tJ5CWXfsXEoW2IDy/ihbRMtiu28Chjw==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.23.4" + } + }, + "node_modules/@astrojs/react": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@astrojs/react/-/react-7.1.1.tgz", + "integrity": "sha512-jHan8UuXzFEbzcmsjCRrepm9Xc5XRz+Jt57MjfE568tbOEoDKVzeMuHf9YO0Y4hHwROX8iJmLzaiT3Yr5W0UbQ==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.13.0", + "@vitejs/plugin-react": "^6.1.2", + "devalue": "^6.0.2", + "ultrahtml": "^1.7.0", + "vite": "^8.3.4" + }, + "engines": { + "node": ">=22.23.4" + }, + "peerDependencies": { + "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", + "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", + "oxc-transform-react": "^0.145.0", + "react": "^17.0.2 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.4.1.tgz", + "integrity": "sha512-SMkvMsGOpM0FMAoT5pzhgI3LZC0JSPWf7Evwop+fZ3N6UkusPM7lI+sOJjMNI5OJrVoiei1/AGo1cY7sikTc5A==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.4.0", + "dset": "^3.1.4", + "is-docker": "^4.0.0", + "package-manager-detector": "^1.9.0" + }, + "engines": { + "node": ">=22.23.4" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", + "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz", + "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bruits/satteri-darwin-arm64": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.10.5.tgz", + "integrity": "sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@bruits/satteri-darwin-x64": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.10.5.tgz", + "integrity": "sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@bruits/satteri-linux-arm64-gnu": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.10.5.tgz", + "integrity": "sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-arm64-musl": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.10.5.tgz", + "integrity": "sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-x64-gnu": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.10.5.tgz", + "integrity": "sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-x64-musl": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.10.5.tgz", + "integrity": "sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-wasm32-wasi": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.10.5.tgz", + "integrity": "sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.2.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@bruits/satteri-win32-arm64-msvc": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.10.5.tgz", + "integrity": "sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@bruits/satteri-win32-x64-msvc": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.10.5.tgz", + "integrity": "sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@capsizecss/unpack": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.1.tgz", + "integrity": "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@clack/core": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz", + "integrity": "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==", + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.8.1.tgz", + "integrity": "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.5.1", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fontsource-variable/hanken-grotesk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/hanken-grotesk/-/hanken-grotesk-5.3.0.tgz", + "integrity": "sha512-DOd0FqL+3cH2dfNy0JtSq9tUfxBz5mLmHMjTU0DOr3E9Zo6wxuatCl53DIPNrSPgCsnuKyM739BTIG5glCLirw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/jetbrains-mono": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz", + "integrity": "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/space-grotesk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/space-grotesk/-/space-grotesk-5.3.0.tgz", + "integrity": "sha512-2IxmvfB08i9vnGB3Ym/AXvhRE+8XOjWMXIyDum03c+tPwH0FUoMNQfGpU8NXPxjbws0Vvss3AH0Zqt4oJBBAdw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.6.tgz", + "integrity": "sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || 2.0.0-beta.2", + "@emnapi/runtime": "^1.7.1 || 2.0.0-beta.2" + } + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz", + "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", + "integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz", + "integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz", + "integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz", + "integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz", + "integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz", + "integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz", + "integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz", + "integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz", + "integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz", + "integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz", + "integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz", + "integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz", + "integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz", + "integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz", + "integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@shikijs/core": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.5.0.tgz", + "integrity": "sha512-6CIgwq+bt2JZ3OSjb/nBkkwEf/CfpHcbam+Mg5Pllco7HAGRQ2/EFhZrrJtbyNTgKwknfVS+llsXGygiDGwJWw==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.5.0", + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.5.0.tgz", + "integrity": "sha512-Ed6UdEj84LyYkwnxM5CvSMWAD+id9ExTYri0/+x41U9zrgNifPSS9lZm0JqBP0VfYI8yr8tY5nbNq9+p9R0kFA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.5.0.tgz", + "integrity": "sha512-dKVmbJecVdB9recl8a/CU4jdpNXyjJQrIcesIkpS70s5HS02ohpnCSpMUbotPnEUzbAcLQ+S4hwZYbkBBiTcJw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.5.0.tgz", + "integrity": "sha512-wwBTqwduXSz0X4v9Qa2EWeFaYJEw1clCncWjoRNQRbVDu3SPRTRWucnouYapKj3b8Q+sk/41Vbc4B5c8nuYzLA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.5.0.tgz", + "integrity": "sha512-UM7ShlA8oz0OWxLLO8BmO9Ip6fVqdkt8yF0EH7nOCqHfrx1eEAzF0ZUVK7L+I8NEgeu1PEWSRlxaiPPeCb4iDg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.5.0.tgz", + "integrity": "sha512-zGb87H3UBbpuI4yoh5UguB1XbU+2BDU3kflM7UfZ+Cy2hWo9lBtqjSVK20gmLY6ukYl040hAypGXMgud9vul8Q==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.5.0.tgz", + "integrity": "sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.1.0.tgz", + "integrity": "sha512-cVBuhDQjBZaNMTwBquYfPydrUP+E3hE6oYGasx8h6ZrODIM7/r+gxLDXdd66oszbrggZHEntdE6j+5gYIVyMWQ==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz", + "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.152.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/am-i-vibing": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/am-i-vibing/-/am-i-vibing-0.5.0.tgz", + "integrity": "sha512-KLcsDhqf1XRrJmJr+9I/C0EX2K+sGrAGZFgkmd84c8TJO9zJBu+/kFInjazu1KfJWFhWDKdRWkpBzEs7s35DVA==", + "license": "MIT", + "dependencies": { + "process-ancestry": "^0.1.0" + }, + "bin": { + "am-i-vibing": "dist/cli.mjs" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/astro": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/astro/-/astro-7.4.2.tgz", + "integrity": "sha512-FAH4cdDlFaVnQGjazLTEIC5vJqGlo5XANc1wIbzQHIqAjJTll/RWTUs+SPNW1JdezG4YGxMZllaE5A41T2kzhA==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler-rs": "^0.5.1", + "@astrojs/internal-helpers": "0.13.0", + "@astrojs/markdown-satteri": "0.4.5", + "@astrojs/telemetry": "3.4.1", + "@capsizecss/unpack": "^4.0.1", + "@clack/prompts": "^1.8.1", + "@oslojs/encoding": "^1.1.0", + "am-i-vibing": "^0.5.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "ci-info": "^4.4.0", + "clsx": "^2.1.1", + "common-ancestor-path": "^2.0.0", + "cookie": "^2.0.1", + "devalue": "^6.0.2", + "diff": "^9.0.0", + "dset": "^3.1.4", + "es-module-lexer": "^3.0.3", + "flattie": "^1.1.1", + "fontace": "~0.4.1", + "get-tsconfig": "5.0.0-beta.6", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.3.0", + "jsonc-parser": "^3.3.1", + "magic-string": "^1.4.3", + "magicast": "^0.5.5", + "mrmime": "^2.0.1", + "neotraverse": "^1.0.1", + "obug": "^3.0.0", + "p-limit": "^7.3.3", + "p-queue": "^9.3.5", + "package-manager-detector": "^1.9.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.7", + "rolldown": "^1.2.13", + "shiki": "^4.5.0", + "smol-toml": "^1.9.1", + "svgo": "^4.1.0", + "tinyclip": "^1.0.3", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "tinyps": "^0.5.0", + "ultrahtml": "^1.7.0", + "unifont": "^1.0.3", + "unstorage": "^1.17.5", + "verkit": "^0.5.0", + "vite": "^8.3.4", + "vitefu": "^1.1.3", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^22.0.0", + "zod": "^4.6.6" + }, + "bin": { + "astro": "bin/astro.mjs" + }, + "engines": { + "node": ">=22.23.4", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.35.4" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.3.0" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/common-ancestor-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", + "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-es": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "license": "MIT" + }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT", + "peer": true + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-6.0.3.tgz", + "integrity": "sha512-3woMEITftG1wKeO/4d/QXH1DVKctTSZg3jNGPpjp9hNr5Hos8MCFF5McTML5ZtgKDObm2R591B0spRp2IVernA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.4.tgz", + "integrity": "sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==", + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fnv1a-64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/fnv1a-64/-/fnv1a-64-0.1.2.tgz", + "integrity": "sha512-mJbcILTPybAR1jdOwt/lVv8N2cffeJFFP+gVbSAwLEx1ujXH27RpPd8Rokec/KX9c76UYIB6Co+H4lQzxPnJfA==", + "license": "MIT" + }, + "node_modules/fontace": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.1.tgz", + "integrity": "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.2" + } + }, + "node_modules/fontkitten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.3.tgz", + "integrity": "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==", + "license": "MIT", + "dependencies": { + "tiny-inflate": "^1.0.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "5.0.0-beta.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.6.tgz", + "integrity": "sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "engines": { + "node": ">=20.20.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "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" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", + "license": "BSD-2-Clause" + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-docker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-4.0.0.tgz", + "integrity": "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/magicast": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", + "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neotraverse": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-1.0.1.tgz", + "integrity": "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-mock-http": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.5.tgz", + "integrity": "sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/p-limit": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.3.tgz", + "integrity": "sha512-SKgVvDU5TNIxQ4r30U75BNLC6a+6GOEjngrk/ysM5+Zu3oJuk1SC5ApzIzY/7PjCEjoVPMWJ0zho/4QUiEB7TQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.5.tgz", + "integrity": "sha512-Lki/zhXFVmDhesBUn/yHRLW9Bte7mE7xoFHzTR8sIomV7a2z+LxYAPkGvdfidNiu4fKSfjtawixxSHcZci5QcQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.2.tgz", + "integrity": "sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", + "integrity": "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==", + "license": "MIT" + }, + "node_modules/piccolore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", + "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/process-ancestry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/process-ancestry/-/process-ancestry-0.1.0.tgz", + "integrity": "sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.1" + } + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rolldown": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz", + "integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.155.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.15", + "@rolldown/binding-android-arm64": "1.2.15", + "@rolldown/binding-darwin-arm64": "1.2.15", + "@rolldown/binding-darwin-x64": "1.2.15", + "@rolldown/binding-freebsd-x64": "1.2.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.15", + "@rolldown/binding-linux-arm64-gnu": "1.2.15", + "@rolldown/binding-linux-arm64-musl": "1.2.15", + "@rolldown/binding-linux-ppc64-gnu": "1.2.15", + "@rolldown/binding-linux-s390x-gnu": "1.2.15", + "@rolldown/binding-linux-x64-gnu": "1.2.15", + "@rolldown/binding-linux-x64-musl": "1.2.15", + "@rolldown/binding-openharmony-arm64": "1.2.15", + "@rolldown/binding-win32-arm64-msvc": "1.2.15", + "@rolldown/binding-win32-x64-msvc": "1.2.15" + } + }, + "node_modules/satteri": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.10.5.tgz", + "integrity": "sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.5", + "@types/hast": "^3.0.5", + "@types/mdast": "^4.0.4", + "@types/unist": "^3.0.3" + }, + "optionalDependencies": { + "@bruits/satteri-darwin-arm64": "0.10.5", + "@bruits/satteri-darwin-x64": "0.10.5", + "@bruits/satteri-linux-arm64-gnu": "0.10.5", + "@bruits/satteri-linux-arm64-musl": "0.10.5", + "@bruits/satteri-linux-x64-gnu": "0.10.5", + "@bruits/satteri-linux-x64-musl": "0.10.5", + "@bruits/satteri-wasm32-wasi": "0.10.5", + "@bruits/satteri-win32-arm64-msvc": "0.10.5", + "@bruits/satteri-win32-x64-msvc": "0.10.5" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/shiki": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.5.0.tgz", + "integrity": "sha512-OcRW72ngQvc+MSVWBRIovC/BTW589iUE/Vxav7VeM4RcY9MUjqB+IR6ILG1k9nuW6LiG/nct9uma4/MBtnZbmQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.5.0", + "@shikijs/engine-javascript": "4.5.0", + "@shikijs/engine-oniguruma": "4.5.0", + "@shikijs/langs": "4.5.0", + "@shikijs/themes": "4.5.0", + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/smol-toml": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz", + "integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "license": "MIT" + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/svgo": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.1.0.tgz", + "integrity": "sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^6.0.0", + "css-tree": "^3.0.1", + "css-what": "^7.0.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "1.6.1" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tinyclip": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-1.0.3.tgz", + "integrity": "sha512-nTv2raKFMzGG6JJp0lpNDsRPdfFE6UHlu2rVEjXo8vkx1ZS3LW2CVXqIaLu8xZlu+6tbFs9V58Qc/Jmk/Ii7wA==", + "license": "MIT", + "engines": { + "node": "^16.14.0 || >= 17.3.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyps": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/tinyps/-/tinyps-0.5.1.tgz", + "integrity": "sha512-B6Ykif5HflC3AdDkS4Zst/2muYVfeXoVMy+KorqH80s/1x4vunIORpNySH681X7fjj+dFM9MDKD6/Hsccjb87Q==", + "license": "MIT" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/ultrahtml": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz", + "integrity": "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==", + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "8.12.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.3.tgz", + "integrity": "sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unifont": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-1.0.3.tgz", + "integrity": "sha512-XGnEwj5sWdCbiBX01mWLSBncS0yMRjnLygau3IfdOlfjMacNIQvTGW4VH7pWBq+gGWArfSef/CensMLjMmIr/A==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.1.0", + "fnv1a-64": "^0.1.2", + "std-env": "^4.2.0", + "undici": "^8.0.0" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.2.0.tgz", + "integrity": "sha512-fpsYs/VVoZxfGY9/j3FQpZmN8wdLJaV1Pt38AaJNZu9uKIHcFU3halCdBveEq1HGKO1OEfJY65w1zvoFV1b3qQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.1.0.tgz", + "integrity": "sha512-vKXxz6O38sKDj1mtx/k2cWvkkFBuYGx6uGrlzBkVeWCRF/F8sdI0j3Wbkz/5rVNRRuVc8UrgRdbCxNhmYXgP3w==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unstorage": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.6.tgz", + "integrity": "sha512-gjKtcmn/KwZuJZO9o2sc0esAQnhmTw0LviDHwevTXbJMYs0dRTp8ZEEWXHU0vTpzbN3cA0yxlTeyXssMp3zDqw==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.11", + "lru-cache": "^11.5.3", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.4" + }, + "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 || ^11.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" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/verkit": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.5.0.tgz", + "integrity": "sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==", + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz", + "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.30", + "rolldown": "~1.2.14", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json index 110dca4..3b2b77b 100644 --- a/package.json +++ b/package.json @@ -3,38 +3,27 @@ "description": "Personal site of Dennis Thiessen — thiessen.io", "version": "1.0.0", "author": "Dennis Thiessen", - "dependencies": { - "gatsby": "^5.12.12", - "gatsby-plugin-manifest": "^5.12.3", - "gatsby-plugin-react-helmet": "^6.12.0", - "gatsby-plugin-remove-serviceworker": "^1.0.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-helmet": "^6.1.0", - "ajv": "^8.12.0", - "ajv-keywords": "^5.1.0", - "schema-utils": "^4.2.0" - }, - "keywords": [ - "gatsby" - ], "license": "MIT", "scripts": { - "clean": "gatsby clean", - "build": "gatsby build", - "develop": "gatsby develop", - "format": "prettier --write '**/*.js'", - "test": "echo \"Error: no test specified\" && exit 1" + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "test": "node --test", + "format": "prettier --write '**/*.{js,jsx,mjs}'" + }, + "dependencies": { + "@astrojs/react": "^7.1.1", + "@fontsource-variable/hanken-grotesk": "^5.3.0", + "@fontsource-variable/jetbrains-mono": "^5.3.0", + "@fontsource-variable/space-grotesk": "^5.3.0", + "astro": "^7.4.2", + "react": "^19.0.0", + "react-dom": "^19.0.0" }, "devDependencies": { "prettier": "^3.1.0" }, "engines": { - "node": ">=18.0.0" - }, - "resolutions": { - "ajv": "^8.12.0", - "ajv-keywords": "^5.1.0", - "schema-utils": "^4.2.0" + "node": ">=22.23.4" } } diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..6b99108 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 0000000..c301b9b Binary files /dev/null and b/public/favicon-32x32.png differ diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png new file mode 100644 index 0000000..1a54dbc Binary files /dev/null and b/public/icons/icon-192.png differ diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png new file mode 100644 index 0000000..40e7b34 Binary files /dev/null and b/public/icons/icon-512.png differ diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..42319b3 --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,20 @@ +{ + "name": "Dennis Thiessen", + "short_name": "D. Thiessen", + "start_url": "/", + "display": "minimal-ui", + "background_color": "#0a0d12", + "theme_color": "#0a0d12", + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/public/og-image.png b/public/og-image.png new file mode 100644 index 0000000..e08c0ed Binary files /dev/null and b/public/og-image.png differ diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..447fd1d --- /dev/null +++ b/public/sw.js @@ -0,0 +1,17 @@ +// Clean-up for returning visitors. An earlier version of this site may have registered a +// service worker in their browser; this file replaces it, clears its caches and removes +// itself. Visitors who never had one are not affected: nothing registers this file. + +self.addEventListener('install', () => self.skipWaiting()) + +self.addEventListener('activate', (event) => { + event.waitUntil( + (async () => { + const names = await caches.keys() + await Promise.all(names.map((name) => caches.delete(name))) + await self.registration.unregister() + const windows = await self.clients.matchAll({ type: 'window' }) + windows.forEach((client) => client.navigate(client.url)) + })() + ) +}) diff --git a/scripts/make-icons.mjs b/scripts/make-icons.mjs new file mode 100644 index 0000000..daa22f1 --- /dev/null +++ b/scripts/make-icons.mjs @@ -0,0 +1,39 @@ +// Regenerates the favicon, touch icon and app icons from public/icons/icon-512.png. +// Run it after changing the source icon: node scripts/make-icons.mjs + +import { writeFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' + +import sharp from 'sharp' + +const root = fileURLToPath(new URL('../public/', import.meta.url)) +const source = `${root}icons/icon-512.png` +const BACKGROUND = '#0a0d12' + +const resize = (size) => sharp(source).resize(size, size) + +await resize(32).toFile(`${root}favicon-32x32.png`) +await resize(192).toFile(`${root}icons/icon-192.png`) +// iOS fills transparent corners with black, so flatten onto the site background. +await resize(180) + .flatten({ background: BACKGROUND }) + .toFile(`${root}apple-touch-icon.png`) + +const manifest = { + name: 'Dennis Thiessen', + short_name: 'D. Thiessen', + start_url: '/', + display: 'minimal-ui', + background_color: BACKGROUND, + theme_color: BACKGROUND, + icons: [ + { src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' }, + { src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' }, + ], +} +await writeFile( + `${root}manifest.webmanifest`, + `${JSON.stringify(manifest, null, 2)}\n` +) + +console.log('icons and manifest written to public/') diff --git a/src/agent/engine.mjs b/src/agent/engine.mjs new file mode 100644 index 0000000..368b5ce --- /dev/null +++ b/src/agent/engine.mjs @@ -0,0 +1,1425 @@ +// The agent: a deterministic retrieval engine over src/data/profile.mjs. +// +// It has no model and makes no network calls. `respond` takes what the visitor typed and +// returns an answer made of blocks (data, not markup), so the same engine can drive the +// terminal, the tests, or one day a model that is only allowed to quote these facts. + +import { + CERTIFICATIONS, + CURRENT, + EDUCATION, + IDENTITY, + KINDS, + LANGUAGES, + NON_CLAIMS, + PROJECTS, + ROLES, + ROUTE, + SKILLS, + SUMMARY, + TIERS, + TOPICS, + WELCOME, +} from '../data/profile.mjs' +import { plainText } from './inline.mjs' +import { choiceCount, sceneView, startScene, stepScene } from './oncall.mjs' + +const MAX_INPUT = 300 + +// ---------- small builders ---------- + +// Visitor text is echoed inside answers, which are parsed for **bold**, `code` and links. +// Strip those characters so typed input can never become markup. +const clean = (text) => String(text).replace(/[*`[\]()]/g, '') + +const p = (text) => ({ t: 'p', text }) +const note = (text) => ({ t: 'note', text }) +const trace = (tool, arg, out) => ({ tool, arg, out }) +const answer = (steps, blocks, followups = []) => ({ + trace: steps, + blocks, + followups, +}) + +const FOLLOW = { + now: { label: 'What is he working on now?', cmd: '/now' }, + career: { label: 'Walk me through the career', cmd: '/experience' }, + stack: { label: 'Show the stack', cmd: '/stack' }, + projects: { label: 'Browse the projects', cmd: '/projects' }, + ai: { label: 'What about AI and ML?', cmd: '/ai' }, + contact: { label: 'How do I reach him?', cmd: '/contact' }, + education: { label: 'Education and certifications', cmd: '/education' }, + ops: { + label: 'Operations and reliability', + cmd: 'operations and reliability', + }, + play: { label: 'Play: handle an alert', cmd: '/oncall' }, + help: { label: 'All commands', cmd: '/help' }, +} + +const pick = (...keys) => keys.map((k) => FOLLOW[k]) + +export const STARTERS = pick( + 'now', + 'career', + 'stack', + 'projects', + 'contact', + 'play' +) + +// ---------- text normalisation and matching ---------- + +const REWRITES = [ + [/c#/g, ' csharp '], + [/c\+\+/g, ' cpp '], + [/\.net\b/g, ' dotnet '], + [/ci\s*\/\s*cd/g, ' cicd '], + [/node\.js/g, ' nodejs '], + [/on[\s-]+call/g, ' oncall '], + [/on[\s-]+duty/g, ' onduty '], + [/fine[\s-]+tun\w*/g, ' finetuning '], + [/self[\s-]+host\w*/g, ' selfhosted '], + [/google\s+cloud/g, ' googlecloud '], + [/unit[\s-]+test\w*/g, ' unittesting '], +] + +export const normalize = (input) => { + let text = String(input).toLowerCase() + for (const [pattern, to] of REWRITES) text = text.replace(pattern, to) + text = text + .replace(/[’']s\b/g, '') + .replace(/[^a-z0-9]+/g, ' ') + .trim() + const tokens = text.split(' ').filter(Boolean) + const set = new Set() + for (const t of tokens) { + set.add(t) + if (t.length > 3 && t.endsWith('s')) set.add(t.slice(0, -1)) + set.add(`${t}s`) + } + return { text, tokens, set } +} + +const hasPhrase = (text, phrase) => ` ${text} `.includes(` ${phrase} `) + +const matches = (norm, alias) => + alias.includes(' ') ? hasPhrase(norm.text, alias) : norm.set.has(alias) + +const levenshtein = (a, b) => { + const row = Array.from({ length: b.length + 1 }, (_, i) => i) + for (let i = 1; i <= a.length; i += 1) { + let prev = row[0] + row[0] = i + for (let j = 1; j <= b.length; j += 1) { + const tmp = row[j] + row[j] = Math.min( + row[j] + 1, + row[j - 1] + 1, + prev + (a[i - 1] === b[j - 1] ? 0 : 1) + ) + prev = tmp + } + } + return row[b.length] +} + +// ---------- entities ---------- + +const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '') + +const skillIndex = SKILLS.map((skill) => ({ + skill, + keys: [...skill.aliases, slug(skill.name)].filter((k) => k.length >= 3), +})) + +const findSkills = (norm) => { + const found = [] + for (const { skill, keys } of skillIndex) { + if (keys.some((k) => matches(norm, k)) && !found.includes(skill)) + found.push(skill) + } + return found +} + +const findNonClaim = (norm) => + NON_CLAIMS.find(([, aliases]) => aliases.some((a) => matches(norm, a))) || + null + +const findProject = (norm) => + PROJECTS.find((proj) => + [proj.id.replace(/-/g, ' '), ...proj.aliases].some((a) => matches(norm, a)) + ) || null + +const findRole = (norm) => + ROLES.find((role) => + [role.id, ...role.aliases, slug(role.company)].some((a) => matches(norm, a)) + ) || null + +const tierOf = (id) => TIERS.find((t) => t.id === id) + +// ---------- answers ---------- + +const aboutAnswer = () => + answer( + [trace('Read', 'profile', `${ROLES.length} roles, ${IDENTITY.experience}`)], + SUMMARY.map(p), + pick('now', 'career', 'stack', 'projects') + ) + +const nowAnswer = () => + answer( + [trace('Read', 'experience/swisscom', 'current role')], + CURRENT.map(p), + [ + { label: 'The Swisscom role in detail', cmd: '/experience swisscom' }, + ...pick('stack', 'ops', 'projects'), + ] + ) + +const timelineBlock = () => ({ + t: 'timeline', + items: ROLES.map(({ id, period, company, place, title, summary, live }) => ({ + id, + period, + company, + place, + title, + summary, + live: Boolean(live), + })), +}) + +const experienceAnswer = (arg) => { + if (!arg) { + return answer( + [trace('Read', 'experience', `${ROLES.length} roles, 2015 to present`)], + [ + { t: 'route', stops: ROUTE }, + timelineBlock(), + note('Open a role by clicking its name, or type /experience bosch.'), + ], + pick('now', 'stack', 'projects', 'education') + ) + } + const role = findRole(normalize(arg)) + if (!role) + return unknownEntity( + 'role', + arg, + ROLES.map((r) => r.id), + '/experience' + ) + const i = ROLES.indexOf(role) + const neighbours = [ROLES[i - 1], ROLES[i + 1]].filter(Boolean) + return answer( + [trace('Read', `experience/${role.id}`, role.period)], + [ + { + t: 'detail', + title: role.company, + meta: [role.title, role.period, role.place], + note: role.titleHistory, + points: role.points, + stack: role.stack, + }, + ], + [ + ...neighbours.map((r) => ({ + label: `${r.company}, ${r.period}`, + cmd: `/experience ${r.id}`, + })), + FOLLOW.career, + ] + ) +} + +const stackAnswer = (arg) => { + if (arg) return lookupAnswer(normalize(arg), arg) + const groups = TIERS.map((tier) => ({ + label: tier.label, + hint: tier.hint, + items: SKILLS.filter((s) => s.tier === tier.id && s.listed !== false).map( + (s) => s.name + ), + })).filter((g) => g.items.length) + return answer( + [ + trace( + 'Read', + 'stack', + `${groups.reduce((n, g) => n + g.items.length, 0)} skills in ${groups.length} tiers` + ), + ], + [ + { t: 'tiers', groups }, + note( + 'Each skill sits where it was really used. Ask about one by name, for example “Kafka” or “Does he know Kubernetes?”' + ), + ], + pick('ai', 'projects', 'career') + ) +} + +const skillsBlock = (skills) => ({ + t: 'skills', + items: skills.map((s) => ({ + name: s.name, + tier: tierOf(s.tier).label, + note: s.note, + })), +}) + +const skillAnswer = (skills) => { + const names = skills.map((s) => s.name) + const related = PROJECTS.filter((proj) => + skills.some((s) => + proj.stack.some( + (x) => slug(x) === slug(s.name) || s.aliases.includes(slug(x)) + ) + ) + ).slice(0, 2) + return answer( + [ + trace( + 'Search', + `stack, “${names.join(', ')}”`, + `${skills.length} ${skills.length === 1 ? 'match' : 'matches'}` + ), + ], + [skillsBlock(skills)], + [ + ...related.map((proj) => ({ + label: proj.name, + cmd: `/projects ${proj.id}`, + })), + FOLLOW.stack, + ] + ) +} + +const nonClaimAnswer = ([label]) => + answer( + [trace('Search', `stack, “${label}”`, 'no match in the record')], + [ + p(`**${label}** isn’t in his listed stack.`), + p( + 'What Dennis does work in is listed under /stack. If this matters for a role, ask him directly.' + ), + ], + pick('stack', 'contact') + ) + +const lookupAnswer = (norm, query) => { + const skills = findSkills(norm) + if (skills.length) return skillAnswer(skills) + const claim = findNonClaim(norm) + if (claim) return nonClaimAnswer(claim) + return answer( + [trace('Search', `stack, “${query}”`, 'no match')], + [ + p( + `Nothing in the stack matches “${clean(query)}”. Here is what is there.` + ), + ], + pick('stack', 'ai', 'contact') + ) +} + +const projectsAnswer = (arg) => { + if (!arg) { + const groups = Object.entries(KINDS) + .map(([kind, label]) => ({ + label, + items: PROJECTS.filter((x) => x.kind === kind).map( + ({ id, name, period, blurb }) => ({ id, name, period, blurb }) + ), + })) + .filter((g) => g.items.length) + return answer( + [trace('Read', 'projects', `${PROJECTS.length} projects`)], + [ + { t: 'projects', groups }, + note('Open one by clicking its name, or type /projects fab-inference.'), + ], + pick('stack', 'career', 'ai') + ) + } + const norm = normalize(arg) + const proj = + findProject(norm) || + PROJECTS.find((x) => normalize(x.name).text.includes(norm.text)) + if (!proj) + return unknownEntity( + 'project', + arg, + PROJECTS.map((x) => x.id), + '/projects' + ) + const i = PROJECTS.indexOf(proj) + const next = PROJECTS[(i + 1) % PROJECTS.length] + return answer( + [trace('Read', `projects/${proj.id}`, KINDS[proj.kind].toLowerCase())], + [ + { + t: 'detail', + title: proj.name, + meta: [KINDS[proj.kind], proj.period], + body: proj.body, + points: proj.points, + scope: proj.scope, + stack: proj.stack, + }, + ], + [ + { label: `Next: ${next.name}`, cmd: `/projects ${next.id}` }, + FOLLOW.projects, + FOLLOW.contact, + ] + ) +} + +const educationAnswer = () => + answer( + [ + trace( + 'Read', + 'education', + `${EDUCATION.length} degrees, ${CERTIFICATIONS.length} certifications` + ), + ], + [ + { + t: 'facts', + heading: 'Degrees', + rows: EDUCATION.map((e) => [e.title, e.detail]), + }, + { + t: 'facts', + heading: 'Certifications', + rows: CERTIFICATIONS.map((c) => [ + c.name, + [c.year, c.note].filter(Boolean).join(', '), + ]), + }, + p( + `**Languages:** ${LANGUAGES.map(([name, level]) => `${name} (${level})`).join(', ')}.` + ), + ], + pick('career', 'ai', 'contact') + ) + +const contactAnswer = (norm) => { + const terms = + norm && + [ + 'salary', + 'rate', + 'availability', + 'available', + 'hire', + 'hiring', + 'freelance', + ].some((w) => norm.set.has(w)) + return answer( + [trace('Read', 'contact', '2 channels')], + [ + ...(terms + ? [ + p( + 'Availability, rates and terms are best discussed with Dennis directly.' + ), + ] + : []), + { + t: 'kv', + rows: [ + ['Email', `[${IDENTITY.email}](mailto:${IDENTITY.email})`], + ['LinkedIn', `[${IDENTITY.linkedin}](${IDENTITY.linkedinUrl})`], + ], + }, + note('Email is the fastest way to reach him.'), + ], + pick('now', 'projects', 'play') + ) +} + +const countFacts = (blocks) => + blocks.reduce((n, b) => { + if (b.t === 'p') return n + 1 + if (b.t === 'list') return n + b.items.length + if (b.t === 'kv') return n + b.rows.length + return n + }, 0) + +const topicAnswer = (key, followups = []) => { + const { blocks } = TOPICS[key] + return answer( + [trace('Read', `topics/${key}`, `${countFacts(blocks)} facts`)], + blocks, + followups + ) +} + +const helpAnswer = () => + answer( + [], + [ + { + t: 'commands', + rows: COMMANDS.map((c) => ({ + usage: `/${c.name}${c.args ? ` ${c.args}` : ''}`, + hint: c.hint, + })), + }, + p( + 'You can also ask in plain language, for example “Does he know Kafka?”, “What has he built?” or “Is he allowed to work in Switzerland?”.' + ), + note('A few shell habits work too: ls, cat about.md, git log.'), + ], + pick('now', 'career', 'play') + ) + +const unknownEntity = (kind, arg, ids, base) => + answer( + [trace('Search', `${kind}s, “${arg}”`, 'no match')], + [ + p( + `No ${kind} matches “${clean(arg)}”. Try one of: ${ids.map((id) => `\`${id}\``).join(', ')}.` + ), + ], + [{ label: `All ${kind}s`, cmd: base }, FOLLOW.help] + ) + +const fallbackAnswer = (input) => + answer( + [ + trace( + 'Search', + `knowledge base, “${input.length > 40 ? `${input.slice(0, 37)}...` : input}”`, + 'no match' + ), + ], + [ + p('That’s outside what I can answer about Dennis’s work.'), + p( + `For anything else, email him at [${IDENTITY.email}](mailto:${IDENTITY.email}).` + ), + ], + pick('now', 'career', 'stack', 'help') + ) + +// ---------- commands ---------- + +const ROLE_IDS = ROLES.map((r) => r.id) +const PROJECT_IDS = PROJECTS.map((x) => x.id) + +export const COMMANDS = [ + { + name: 'about', + aliases: ['whoami', 'me', 'summary', 'intro'], + hint: 'Who Dennis is', + run: () => aboutAnswer(), + }, + { + name: 'now', + aliases: ['current', 'today'], + hint: 'What he is working on', + run: () => nowAnswer(), + }, + { + name: 'experience', + aliases: ['career', 'exp', 'work', 'roles', 'history'], + args: '[company]', + hint: 'The career, role by role', + options: ROLE_IDS, + run: (arg) => experienceAnswer(arg), + }, + { + name: 'stack', + aliases: ['skills', 'tech', 'tools'], + args: '[skill]', + hint: 'Skills, by how they were used', + run: (arg) => stackAnswer(arg), + }, + { + name: 'projects', + aliases: ['portfolio', 'project'], + args: '[name]', + hint: 'Things he has built', + options: PROJECT_IDS, + run: (arg) => projectsAnswer(arg), + }, + { + name: 'education', + aliases: ['edu', 'certs', 'certifications', 'degree', 'studies'], + hint: 'Degrees, certifications, languages', + run: () => educationAnswer(), + }, + { + name: 'ai', + aliases: ['llm', 'ml'], + hint: 'What he has done with AI and ML', + run: () => topicAnswer('ai', pick('projects', 'stack', 'contact')), + }, + { + name: 'contact', + aliases: ['email', 'reach', 'hire', 'linkedin'], + hint: 'How to reach him', + run: () => contactAnswer(null), + }, + { + name: 'oncall', + aliases: ['play', 'game'], + hint: 'A short game: handle an alert', + scene: true, + }, + { + name: 'theme', + aliases: [], + args: '[light|dark]', + hint: 'Change the colours', + options: ['light', 'dark'], + }, + { + name: 'classic', + aliases: ['plain', 'cv', 'text'], + hint: 'Switch to the plain page', + run: () => + answer( + [], + [ + p( + 'Switching to the classic view. Type /about or reload to come back to the terminal.' + ), + ] + ), + }, + { + name: 'site', + aliases: ['how', 'privacy'], + hint: 'How this terminal works', + run: () => topicAnswer('site', pick('stack', 'contact')), + }, + { + name: 'help', + aliases: ['?', 'commands'], + hint: 'This list', + run: () => helpAnswer(), + }, + { name: 'clear', aliases: ['cls'], hint: 'Clear the screen' }, +] + +export const resolveCommand = (name) => { + const n = name.toLowerCase() + const exact = COMMANDS.find((c) => c.name === n || c.aliases.includes(n)) + if (exact) return { command: exact } + const prefixed = COMMANDS.filter((c) => c.name.startsWith(n)) + if (prefixed.length === 1) return { command: prefixed[0] } + if (prefixed.length > 1) return { ambiguous: prefixed } + const near = COMMANDS.filter((c) => levenshtein(c.name, n) <= 2) + return { near } +} + +// Suggestions for the autocomplete popup. +export const completions = (value) => { + if (!value.startsWith('/')) return [] + const [head, ...rest] = value.slice(1).split(' ') + if (rest.length === 0) { + return COMMANDS.filter((c) => c.name.startsWith(head.toLowerCase())).map( + (c) => ({ + label: `/${c.name}${c.args ? ` ${c.args}` : ''}`, + hint: c.hint, + insert: `/${c.name}${c.args ? ' ' : ''}`, + }) + ) + } + const found = resolveCommand(head).command + if (!found || !found.options) return [] + const typed = rest.join(' ').toLowerCase() + return found.options + .filter((o) => o.startsWith(typed) && o !== typed) + .map((o) => ({ + label: `/${found.name} ${o}`, + hint: '', + insert: `/${found.name} ${o}`, + })) +} + +// ---------- shell habits ---------- + +const FILES = [ + ['about.md', '/about'], + ['now.md', '/now'], + ['experience/', '/experience'], + ['stack.json', '/stack'], + ['projects/', '/projects'], + ['education.md', '/education'], + ['contact.txt', '/contact'], +] + +const shortHash = (s) => { + let h = 2166136261 + for (const ch of s) h = Math.imul(h ^ ch.charCodeAt(0), 16777619) + return (h >>> 0).toString(16).padStart(8, '0').slice(0, 7) +} + +const shell = (input) => { + const [cmd, ...args] = input.trim().split(/\s+/) + const first = args[0] || '' + const say = (text, follow = pick('help')) => ({ + answer: answer([], [{ t: 'pre', lines: [text] }], follow), + }) + switch (cmd.toLowerCase()) { + case 'ls': { + const dir = first.replace(/\/$/, '') + if (dir === 'experience') + return { + answer: answer( + [], + [{ t: 'pre', lines: ROLE_IDS.map((id) => `${id}.md`) }], + pick('career') + ), + } + if (dir === 'projects') + return { + answer: answer( + [], + [{ t: 'pre', lines: PROJECT_IDS.map((id) => `${id}.md`) }], + pick('projects') + ), + } + if (dir && dir !== '.' && dir !== '~') + return say(`ls: cannot access '${first}': No such file or directory`) + return { + answer: answer( + [], + [{ t: 'pre', lines: FILES.map(([name]) => name) }], + pick('help', 'career') + ), + } + } + case 'cat': { + if (!first) return say('cat: missing file operand') + const path = first.replace(/^\.\//, '').replace(/\/$/, '') + const [dir, file] = path.split('/') + const base = (file || dir).replace(/\.(md|json|txt)$/, '') + if (dir === 'experience' && file && ROLE_IDS.includes(base)) + return { rerun: `/experience ${base}` } + if (dir === 'projects' && file && PROJECT_IDS.includes(base)) + return { rerun: `/projects ${base}` } + const hit = + !file && + FILES.find( + ([name]) => name.replace(/\.(md|json|txt)$|\/$/g, '') === base + ) + if (hit) return { rerun: hit[1] } + return say(`cat: ${first}: No such file or directory`) + } + case 'pwd': + return say('/home/visitor/thiessen.io') + case 'cd': + return say('This is a read-only session. Try ls.') + case 'echo': + return say(args.join(' ')) + case 'sudo': + return say( + 'dennis is not in the sudoers file. This incident will be reported. Try /contact instead.', + pick('contact') + ) + case 'rm': + return say('Read-only session: nothing was deleted. Nice try, though.') + case 'vim': + case 'vi': + case 'nano': + case 'emacs': + return say('No editors here: this session is read-only.') + case 'exit': + case 'quit': + case 'logout': + return say( + 'There is no exit, but the tab has a close button. To continue with a human, try /contact.', + pick('contact') + ) + case 'man': + return { rerun: '/help' } + case 'git': { + const lines = ROLES.map( + (r) => + `${shortHash(r.id)} ${r.period.padEnd(15)} ${r.title}, ${r.company}` + ) + return { + answer: answer([], [{ t: 'pre', lines }], pick('career', 'stack')), + } + } + default: + return null + } +} + +const SHELL_WORDS = new Set([ + 'ls', + 'cat', + 'pwd', + 'cd', + 'echo', + 'sudo', + 'rm', + 'vim', + 'vi', + 'nano', + 'emacs', + 'exit', + 'quit', + 'logout', + 'man', + 'git', +]) + +// ---------- intents for plain language ---------- + +const INTENTS = [ + { + id: 'greet', + short: true, + words: { + hi: 3, + hello: 3, + hey: 3, + hallo: 3, + moin: 3, + servus: 3, + greeting: 3, + hola: 3, + }, + }, + { + id: 'thanks', + short: true, + words: { thanks: 3, thank: 3, danke: 3, cheers: 3, thx: 3 }, + }, + { + id: 'jailbreak', + words: { jailbreak: 5 }, + phrases: { + 'ignore previous': 5, + 'ignore all': 5, + 'ignore your': 5, + 'system prompt': 5, + 'your instructions': 4, + 'developer mode': 5, + 'reveal your prompt': 5, + 'pretend you': 3, + }, + }, + { + id: 'site', + words: { chatbot: 3, bot: 2, scripted: 3, fake: 2, llm: 0 }, + phrases: { + 'are you real': 4, + 'are you an ai': 5, + 'are you a bot': 5, + 'are you an llm': 5, + 'are you a real': 4, + 'how does this work': 5, + 'how do you work': 5, + 'how was this built': 5, + 'how is this built': 5, + 'what model': 4, + 'which model': 4, + 'who are you': 4, + 'what are you': 4, + 'who built this': 4, + 'source code': 3, + 'this website': 3, + 'this site': 3, + 'this terminal': 3, + 'powered by': 3, + }, + }, + { + id: 'help', + words: { help: 3, command: 3, option: 2, menu: 2 }, + phrases: { + 'what can you do': 5, + 'what can i ask': 5, + 'what do you know': 4, + 'how do i use': 4, + }, + }, + { + id: 'about', + words: { + who: 2, + introduce: 2, + summary: 2, + overview: 2, + bio: 3, + profile: 2, + yourself: 1, + dennis: 2, + thiessen: 2, + about: 1, + }, + phrases: { + 'tell me about dennis': 4, + 'who is dennis': 5, + 'what does he do': 4, + 'what does dennis do': 5, + 'what do you do': 2, + 'how many years': 5, + 'years of experience': 5, + 'how long has': 4, + 'how experienced': 4, + }, + }, + { + id: 'now', + words: { + now: 2, + currently: 3, + current: 2, + today: 2, + present: 1, + working: 2, + lately: 2, + employer: 3, + recent: 1, + }, + phrases: { + 'working on': 4, + 'what is he doing': 4, + 'where does he work': 5, + 'where is he working': 5, + 'who does he work for': 5, + 'current role': 4, + 'current job': 4, + 'current employer': 4, + 'latest role': 4, + }, + }, + { + id: 'experience', + words: { + experience: 3, + career: 3, + role: 3, + job: 3, + history: 2, + worked: 2, + company: 2, + timeline: 3, + resume: 2, + cv: 2, + journey: 2, + route: 2, + employment: 3, + }, + phrases: { + 'walk me through': 3, + 'work history': 4, + 'where has he worked': 5, + 'previous jobs': 4, + }, + }, + { + id: 'stack', + words: { + stack: 3, + skill: 3, + tech: 2, + technology: 3, + tool: 2, + tooling: 2, + expertise: 3, + proficient: 2, + programming: 2, + framework: 2, + strength: 2, + language: 1, + good: 1, + }, + phrases: { + 'what can he do': 3, + 'programming languages': 4, + 'tech stack': 4, + 'what is he good at': 5, + 'best at': 3, + }, + }, + { + id: 'projects', + words: { + project: 3, + portfolio: 3, + built: 1, + build: 1, + github: 3, + showcase: 2, + demo: 2, + example: 2, + accomplishment: 3, + achievement: 3, + proud: 2, + }, + phrases: { + 'side project': 4, + 'what has he built': 5, + 'what did he build': 5, + 'what has dennis built': 5, + 'work samples': 4, + }, + }, + { + id: 'education', + words: { + education: 3, + degree: 3, + university: 3, + studied: 3, + study: 2, + school: 2, + master: 2, + bachelor: 2, + certification: 3, + certificate: 3, + certified: 2, + cert: 3, + diploma: 2, + academic: 2, + college: 2, + thesis: 3, + tongji: 3, + }, + phrases: { 'where did he study': 5 }, + }, + { + id: 'ai', + words: { + ai: 3, + ml: 3, + llm: 3, + genai: 3, + agent: 1, + neural: 2, + mlops: 3, + generative: 2, + artificial: 2, + intelligence: 2, + }, + phrases: { 'machine learning': 4, 'deep learning': 4, 'large language': 3 }, + }, + { + id: 'mentoring', + words: { + mentor: 3, + mentoring: 3, + mentored: 3, + leader: 2, + leadership: 3, + manage: 2, + management: 2, + junior: 3, + supervise: 2, + supervision: 2, + teach: 2, + coach: 2, + onboarding: 2, + lead: 1, + }, + phrases: { 'lead a team': 4, 'people management': 4, 'team player': 2 }, + }, + { + id: 'reliability', + words: { + oncall: 3, + onduty: 3, + incident: 3, + reliability: 3, + monitoring: 2, + alerting: 3, + alert: 3, + ops: 2, + operations: 2, + sre: 3, + observability: 3, + pager: 2, + slo: 2, + uptime: 2, + devops: 2, + }, + }, + { + id: 'testing', + words: { + testing: 3, + test: 2, + quality: 1, + tdd: 3, + unittesting: 3, + qa: 2, + bdd: 0, + }, + }, + { + id: 'languages', + words: { + speak: 3, + spoken: 3, + german: 2, + english: 2, + norwegian: 3, + russian: 3, + bilingual: 3, + native: 2, + }, + phrases: { 'what languages': 2 }, + }, + { + id: 'work', + words: { + based: 2, + location: 3, + relocate: 3, + relocation: 3, + remote: 3, + hybrid: 2, + onsite: 2, + live: 1, + where: 1, + }, + phrases: { + 'where is he': 3, + 'where does he live': 5, + 'where is he based': 5, + }, + }, + { + id: 'contact', + words: { + contact: 3, + email: 3, + mail: 2, + reach: 3, + linkedin: 3, + phone: 3, + call: 2, + hire: 3, + hiring: 3, + recruit: 3, + recruiter: 3, + available: 2, + availability: 3, + salary: 3, + rate: 2, + interview: 2, + connect: 2, + message: 2, + freelance: 3, + opportunity: 2, + vacancy: 3, + }, + phrases: { 'get in touch': 5, 'open to': 3, 'reach out': 4 }, + }, +] + +const MIN_SCORE = 2 + +const scoreIntent = (intent, norm) => { + let s = 0 + for (const [w, weight] of Object.entries(intent.words || {})) + if (norm.set.has(w)) s += weight + for (const [ph, weight] of Object.entries(intent.phrases || {})) + if (hasPhrase(norm.text, ph)) s += weight + return s +} + +export const classify = (norm) => { + let best = null + for (const intent of INTENTS) { + if (intent.short && norm.tokens.length > 3) continue + const s = scoreIntent(intent, norm) + if (s >= MIN_SCORE && (!best || s > best.score)) + best = { id: intent.id, score: s } + } + return best +} + +const runIntent = (id, norm, input) => { + switch (id) { + case 'greet': + return answer( + [], + [p('Hello. Ask about Dennis’s work, or pick one of the suggestions.')], + STARTERS + ) + case 'thanks': + return answer([], [p('Anytime.')], pick('contact', 'projects')) + case 'jailbreak': + return answer( + [], + [ + p( + 'There is no system prompt to leak and no instruction to override. This is a lookup table with good manners. Try /help.' + ), + ], + pick('help', 'site') + ) + case 'site': + return COMMANDS.find((c) => c.name === 'site').run() + case 'help': + return helpAnswer() + case 'about': + return aboutAnswer() + case 'now': + return nowAnswer() + case 'experience': + return experienceAnswer('') + case 'stack': + return stackAnswer('') + case 'projects': + return projectsAnswer('') + case 'education': + return educationAnswer() + case 'ai': + return COMMANDS.find((c) => c.name === 'ai').run() + case 'mentoring': + return topicAnswer('mentoring', pick('career', 'contact')) + case 'reliability': + return topicAnswer('reliability', pick('now', 'play', 'career')) + case 'testing': + return topicAnswer('testing', pick('career', 'stack')) + case 'languages': + return answer( + [trace('Read', 'languages', `${LANGUAGES.length} languages`)], + [{ t: 'kv', rows: LANGUAGES }], + pick('education', 'contact') + ) + case 'work': + return topicAnswer('work', pick('contact', 'career')) + case 'contact': + return contactAnswer(norm) + default: + return fallbackAnswer(input) + } +} + +const routePlain = (input) => { + const norm = normalize(input) + if (!norm.tokens.length) return helpAnswer() + + const skills = findSkills(norm) + if (skills.length) return skillAnswer(skills) + const claim = findNonClaim(norm) + if (claim) return nonClaimAnswer(claim) + + const proj = findProject(norm) + if (proj) return projectsAnswer(proj.id) + const role = findRole(norm) + if (role) return experienceAnswer(role.id) + const intent = classify(norm) + return runIntent(intent ? intent.id : null, norm, input) +} + +// ---------- entry points ---------- + +const sceneAnswer = (scene) => { + const view = sceneView(scene) + const followups = view.done + ? [{ label: 'Play again', cmd: '/oncall' }, FOLLOW.ops, FOLLOW.contact] + : view.choices.map((text, i) => ({ + label: text, + cmd: String(i + 1), + echo: text, + })) + const steps = + scene.node === 'start' ? [trace('Run', 'scenario/alert', 'fictional')] : [] + return { + answer: answer(steps, view.blocks, followups), + scene: view.done ? null : scene, + } +} + +const LEAVE_SCENE = new Set(['/exit', '/quit', '/stop', '/q']) + +// Whole inputs that mean a command, typed without the slash. +const BARE = { + clear: '/clear', + cls: '/clear', + classic: '/classic', + 'classic view': '/classic', + plain: '/classic', + 'plain text': '/classic', + dark: '/theme dark', + 'dark mode': '/theme dark', + light: '/theme light', + 'light mode': '/theme light', + play: '/oncall', + game: '/oncall', +} + +const runCommand = (command, arg) => { + if (command.name === 'oncall') return sceneAnswer(startScene()) + if (command.name === 'clear') + return { answer: null, action: { type: 'clear' } } + if (command.name === 'theme') { + const value = arg.toLowerCase() + if (!arg) + return { + answer: answer([], [p('Theme switched.')]), + action: { type: 'theme', value: 'toggle' }, + } + if (!['light', 'dark'].includes(value)) { + return { + answer: answer([], [p('Use /theme light or /theme dark.')]), + } + } + return { + answer: answer([], [p(`Theme set to ${value}.`)]), + action: { type: 'theme', value }, + } + } + const result = { answer: command.run(arg) } + if (command.name === 'classic') result.action = { type: 'classic' } + return result +} + +const runSlash = (text) => { + const [name = '', ...rest] = text.trim().split(/\s+/) + if (!name) return { answer: helpAnswer() } + const arg = rest.join(' ') + const found = resolveCommand(name) + if (found.command) return runCommand(found.command, arg) + const list = found.ambiguous || found.near || [] + const suggestion = list.length + ? `Did you mean ${list.map((c) => `/${c.name}`).join(' or ')}?` + : 'Type /help to see what is available.' + return { + answer: answer( + [], + [p(`Unknown command /${clean(name)}. ${suggestion}`)], + list.length + ? list.map((c) => ({ label: `/${c.name}`, cmd: `/${c.name}` })) + : pick('help') + ), + } +} + +// `scene` is the active scenario state, or null. Returns { answer, action?, scene }. +export const respond = (rawInput, scene = null) => { + const input = String(rawInput ?? '') + .trim() + .slice(0, MAX_INPUT) + if (!input) return { answer: null, scene } + + if (input.startsWith('/')) { + if (scene && LEAVE_SCENE.has(input.toLowerCase())) { + return { + answer: answer( + [], + [p('Left the scenario.')], + pick('now', 'career', 'projects') + ), + scene: null, + } + } + const out = runSlash(input.slice(1)) + return { scene: null, ...out } + } + + if (scene) { + const next = stepScene(scene, input) + if (!next) { + const n = choiceCount(scene) + return { + answer: answer( + [], + [p(`Choose 1 to ${n}, or type /exit to leave.`)], + sceneAnswer(scene).answer.followups + ), + scene, + } + } + return sceneAnswer(next) + } + + const bare = BARE[normalize(input).text] + if (bare) return respond(bare, null) + + const word = input.split(/\s+/)[0].toLowerCase() + if (SHELL_WORDS.has(word)) { + const out = shell(input) + if (out && out.rerun) return respond(out.rerun, null) + if (out) return { scene: null, ...out } + } + + return { answer: routePlain(input), scene: null } +} + +export const INTRO = { + ...aboutAnswer(), + followups: STARTERS, +} + +export { WELCOME } + +// A plain-text version of an answer: for screen readers and for tests. +export const answerToText = (a) => + a.blocks + .map((b) => { + switch (b.t) { + case 'p': + case 'note': + return plainText(b.text) + case 'list': + case 'choices': + return b.items.map(plainText).join('\n') + case 'kv': + return b.rows.map(([k, v]) => `${k}: ${plainText(v)}`).join('\n') + case 'facts': + return `${b.heading}\n${b.rows.map(([k, v]) => `${k}: ${plainText(v)}`).join('\n')}` + case 'pre': + return b.lines.join('\n') + case 'route': + return b.stops.map((s) => `${s.place} (${s.note})`).join(', ') + case 'timeline': + return b.items + .map((r) => `${r.period}: ${r.title}, ${r.company}. ${r.summary}`) + .join('\n') + case 'tiers': + return b.groups + .map((g) => `${g.label}: ${g.items.join(', ')}`) + .join('\n') + case 'skills': + return b.items + .map((s) => `${s.name} (${s.tier}): ${s.note}`) + .join('\n') + case 'projects': + return b.groups + .flatMap((g) => + g.items.map((i) => `${i.name}, ${i.period}: ${i.blurb}`) + ) + .join('\n') + case 'detail': + return [ + b.title, + b.meta.join(', '), + b.note, + ...(b.body || []), + ...(b.points || []), + b.scope, + (b.stack || []).join(', '), + ] + .filter(Boolean) + .map(plainText) + .join('\n') + case 'commands': + return b.rows.map((r) => `${r.usage}: ${r.hint}`).join('\n') + case 'ending': + return `${b.title}. ${b.text}` + default: + return '' + } + }) + .filter(Boolean) + .join('\n\n') diff --git a/src/agent/inline.mjs b/src/agent/inline.mjs new file mode 100644 index 0000000..3062cad --- /dev/null +++ b/src/agent/inline.mjs @@ -0,0 +1,38 @@ +// Inline markup for answer text: **bold**, `code` and [label](href). + +const TOKEN = /\*\*(.+?)\*\*|`([^`]+)`|\[([^\]]+)\]\(([^)]+)\)/g + +export const parseInline = (text) => { + const out = [] + let last = 0 + for (const m of text.matchAll(TOKEN)) { + if (m.index > last) out.push({ k: 'text', v: text.slice(last, m.index) }) + if (m[1] !== undefined) out.push({ k: 'strong', v: m[1] }) + else if (m[2] !== undefined) out.push({ k: 'code', v: m[2] }) + else out.push({ k: 'link', v: m[3], href: m[4] }) + last = m.index + m[0].length + } + if (last < text.length) out.push({ k: 'text', v: text.slice(last) }) + return out +} + +export const plainText = (text) => + parseInline(text) + .map((seg) => seg.v) + .join('') + +// Keep only the first `count` visible characters, for the typing effect. +export const takeChars = (segments, count) => { + const out = [] + let left = count + for (const seg of segments) { + if (left <= 0) break + out.push(seg.v.length <= left ? seg : { ...seg, v: seg.v.slice(0, left) }) + left -= seg.v.length + } + return out +} + +// The only link targets the terminal will render as links. +export const isSafeHref = (href) => + /^(https:\/\/|mailto:|#classic$)/i.test(href) diff --git a/src/agent/oncall.mjs b/src/agent/oncall.mjs new file mode 100644 index 0000000..e36966c --- /dev/null +++ b/src/agent/oncall.mjs @@ -0,0 +1,156 @@ +// A short choose-your-path scene. It is fiction, not an incident log: it only shows how +// Dennis cares about reliability (diagnose before re-running, idempotent jobs, a check +// for every incident). + +const NODES = { + start: { + text: [ + 'A data-quality alert fires on one of your pipelines: today’s partition has far fewer rows than the usual range. The dashboards that read this table refresh soon.', + 'This is a fictional scenario. What do you do first?', + ], + choices: [ + { text: 'Re-run the job right away.', next: 'rerun' }, + { + text: 'Compare against the source: row counts, schema, arrival time.', + next: 'inspect', + }, + { text: 'Silence the alert and look again tomorrow.', next: 'silence' }, + ], + }, + rerun: { + text: [ + 'The re-run finishes with the same low count. The partition has now been overwritten twice, and the evidence from the first run is gone. A re-run is not a diagnosis.', + ], + choices: [ + { + text: 'Stop and compare against the source.', + next: 'inspect', + detour: true, + }, + { text: 'Re-run once more, in case it was a fluke.', next: 'paged' }, + ], + }, + silence: { + text: [ + 'An hour later a stakeholder asks why the dashboard looks wrong. The alert did its job. You muted it.', + ], + choices: [ + { + text: 'Reopen the alert and compare against the source.', + next: 'inspect', + detour: true, + }, + { text: 'Tell them it will probably fix itself.', next: 'paged' }, + ], + }, + inspect: { + text: [ + 'The source row counts are normal. The extract landed two hours late, and the job read a partial file.', + ], + choices: [ + { + text: 'Wait for the full extract, confirm the job is idempotent, then re-run.', + next: 'fixed', + }, + { + text: 'Patch the target table by hand so the numbers look right.', + next: 'patch', + }, + ], + }, + patch: { + text: [ + 'The numbers look right until the next load, when the hand-patched rows collide with the real ones. A manual edit to a pipeline’s output is debt with interest.', + ], + choices: [ + { + text: 'Revert it, wait for the full extract and re-run.', + next: 'fixed', + detour: true, + }, + ], + }, + fixed: { + text: [ + 'Counts are back in the usual range. The ticket gets the timeline, the cause and a follow-up. What is the follow-up?', + ], + choices: [ + { + text: 'A completeness check on the source file, before the job starts.', + next: 'end-check', + }, + { text: 'None. Close the ticket and move on.', next: 'end-open' }, + ], + }, +} + +const ENDINGS = { + 'end-check': (detours) => + detours === 0 + ? { + title: 'Clean recovery', + text: 'You diagnosed first, kept the re-run safe and turned the incident into a check.', + } + : { + title: 'Recovered, with a detour', + text: 'You got there, and the check means the next late file stops at the door.', + }, + 'end-open': () => ({ + title: 'Recovered, and it will page again', + text: 'The data is fixed, the cause is not. Next late extract, same alert.', + }), + paged: () => ({ + title: 'Escalated by a stakeholder', + text: 'The problem found you before the monitoring did the job it was built for.', + }), +} + +const MORAL = + 'The takeaway: diagnose before you re-run, keep jobs idempotent so a re-run is safe, and turn every incident into a check. It is the kind of reliability Dennis cares about.' + +export const startScene = () => ({ id: 'oncall', node: 'start', detours: 0 }) + +// Turn a scene state into the blocks to show and the choices to offer. +export const sceneView = (scene) => { + const node = NODES[scene.node] + if (node) { + return { + blocks: [ + ...node.text.map((text) => ({ t: 'p', text })), + { t: 'choices', items: node.choices.map((c) => c.text) }, + ], + choices: node.choices.map((c) => c.text), + done: false, + } + } + const end = ENDINGS[scene.node](scene.detours) + return { + blocks: [ + { t: 'ending', title: end.title, text: end.text }, + { t: 'p', text: MORAL }, + ], + choices: [], + done: true, + } +} + +// Pick a choice by number or by words from its text. Returns the next scene, or null. +export const stepScene = (scene, input) => { + const node = NODES[scene.node] + if (!node) return null + const text = input.trim().toLowerCase() + let index = /^\d+$/.test(text) ? Number(text) - 1 : -1 + if (index < 0) { + index = node.choices.findIndex((c) => c.text.toLowerCase() === text) + } + const choice = node.choices[index] + if (!choice) return null + return { + ...scene, + node: choice.next, + detours: scene.detours + (choice.detour ? 1 : 0), + } +} + +export const choiceCount = (scene) => + NODES[scene.node] ? NODES[scene.node].choices.length : 0 diff --git a/src/assets/images/.gitignore b/src/assets/images/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/src/assets/images/profile_image.jpg b/src/assets/images/profile_image.jpg deleted file mode 100644 index 527fe26..0000000 Binary files a/src/assets/images/profile_image.jpg and /dev/null differ diff --git a/src/assets/images/website-icon-dt.png b/src/assets/images/website-icon-dt.png deleted file mode 100755 index 297b912..0000000 Binary files a/src/assets/images/website-icon-dt.png and /dev/null differ diff --git a/src/assets/style.css b/src/assets/style.css index 29795ca..60a0615 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -1,30 +1,58 @@ /* thiessen.io personal site - Swiss-minimal direction. Display: Space Grotesk · Body: Hanken Grotesk · Mono: Space Mono. - One accent (signal cobalt), used only for links and the live timeline node. + A dark terminal. Mono for everything the agent says (JetBrains Mono), Space Grotesk for titles, + Hanken Grotesk for the plain view. Colour carries meaning, like an editor theme: + mint = you and anything live, blue = the agent's actions and links, amber = the game. + Two views share these tokens: the terminal (.term, .b-*) and the plain page (.wrap, .section). */ :root { + color-scheme: dark; + + --paper: #0a0d12; + --paper-2: #10141b; + --ink: #d6dde8; + --graphite: #9aa5b5; + --graphite-2: #7a8596; + --mist: #171c25; + --rule: #232a36; + --sheen: rgba(255, 255, 255, 0.04); + --signal: #62f0a8; + --signal-weak: rgba(98, 240, 168, 0.12); + --link: #8db2ff; + --amber: #f2c46d; + + --font-display: 'Space Grotesk Variable', 'Hanken Grotesk Variable', system-ui, sans-serif; + --font-body: 'Hanken Grotesk Variable', system-ui, -apple-system, sans-serif; + --font-mono: 'JetBrains Mono Variable', ui-monospace, 'SFMono-Regular', Menlo, monospace; + + --wrap: 1100px; + --col: 820px; +} + +:root[data-theme='light'] { + color-scheme: light; + --paper: #f4f5f7; --paper-2: #ffffff; --ink: #15171c; - --graphite: #565d66; - --graphite-2: #8a919b; - --mist: #e4e7ec; - --signal: #1f44f0; - --signal-weak: rgba(31, 68, 240, 0.10); - - --font-display: 'Space Grotesk', 'Hanken Grotesk', system-ui, sans-serif; - --font-body: 'Hanken Grotesk', system-ui, -apple-system, sans-serif; - --font-mono: 'Space Mono', ui-monospace, 'SFMono-Regular', monospace; - - --wrap: 1100px; + --graphite: #4b525c; + --graphite-2: #646b75; + --mist: #e8ebf0; + --rule: #d3d8df; + --sheen: rgba(255, 255, 255, 0.8); + --signal: #0a7a52; + --signal-weak: rgba(10, 122, 82, 0.10); + --link: #1f44f0; + --amber: #915f00; } /* ---------- reset ---------- */ *, *::before, *::after { box-sizing: border-box; } +[hidden] { display: none !important; } + html { -webkit-text-size-adjust: 100%; } body { @@ -43,7 +71,9 @@ img { display: block; max-width: 100%; } a { color: inherit; } -::selection { background: var(--signal); color: #fff; } +button { font: inherit; } + +::selection { background: var(--signal); color: var(--paper); } :focus-visible { outline: 2px solid var(--signal); @@ -51,7 +81,749 @@ a { color: inherit; } border-radius: 1px; } -/* ---------- layout ---------- */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + border: 0; +} + +/* ---------- frame ---------- */ + +.app { + display: flex; + flex-direction: column; + min-height: 100vh; + min-height: 100dvh; +} + +.chrome { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + width: 100%; + max-width: var(--wrap); + margin: 0 auto; + padding: 1rem clamp(1.25rem, 5vw, 3.5rem); + font-family: var(--font-mono); + font-size: 0.78rem; +} + +.app[data-view='terminal'] .chrome { + max-width: var(--col); + padding-inline: clamp(1.25rem, 5vw, 2rem); +} + +.chrome__id { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.1rem 0.9rem; + margin: 0; +} + +.chrome__id strong { + display: inline-flex; + align-items: center; + gap: 0.55rem; + color: var(--ink); + font-weight: 700; +} + +.chrome__id strong::before { + content: ''; + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background: var(--signal); + box-shadow: 0 0 0 3px var(--signal-weak); +} + +.chrome__id span { color: var(--graphite-2); } + +.chrome__tools { display: flex; gap: 1.4rem; } + +.chrome__tools button { + display: inline-flex; + align-items: center; + gap: 0.45rem; + padding: 0.3rem 0; + background: none; + border: 0; + border-bottom: 1px solid transparent; + color: var(--graphite); + font: inherit; + cursor: pointer; + transition: color 0.15s ease, border-color 0.15s ease; +} + +.chrome__tools button:hover { color: var(--signal); border-bottom-color: var(--signal); } + +.theme-icon { + width: 0.75rem; + height: 0.75rem; + border-radius: 50%; + border: 1.5px solid currentColor; + background: linear-gradient(90deg, currentColor 50%, transparent 50%); +} + +.stage { flex: 1; display: flex; flex-direction: column; } + +.stage__terminal { flex: 1; display: flex; flex-direction: column; } + +/* ---------- terminal ---------- */ + +.term { + flex: 1; + display: flex; + flex-direction: column; + width: 100%; + max-width: var(--col); + margin: 0 auto; + padding: 0 clamp(1.25rem, 5vw, 2rem); + font-family: var(--font-mono); + font-size: 0.9rem; + line-height: 1.7; + font-variant-ligatures: none; +} + +.transcript { flex: 1; padding: 0.5rem 0 2rem; } + +.welcome { + margin: 0 0 2.25rem; + padding: 1.25rem 1.4rem 1.35rem; + background: var(--paper-2); + border: 1px solid var(--rule); + border-radius: 10px; + box-shadow: inset 0 1px 0 var(--sheen); +} + +.welcome__boot { + margin: 0 0 1.1rem; + padding-bottom: 0.8rem; + border-bottom: 1px dashed var(--rule); + color: var(--graphite-2); + font-size: 0.78rem; + letter-spacing: 0.02em; +} + +.welcome__boot::before { content: '$ '; color: var(--signal); } + +.welcome h1 { + margin: 0 0 0.45rem; + font-family: var(--font-display); + font-weight: 500; + font-size: clamp(1.3rem, 3.6vw, 1.75rem); + line-height: 1.15; + letter-spacing: -0.02em; +} + +.welcome p { margin: 0 0 0.25rem; color: var(--graphite); max-width: 60ch; } + +.welcome .welcome__hint { max-width: none; margin: 0.6rem 0 0; color: var(--ink); font-size: 0.8rem; } + +.chevron { + display: inline-block; + flex: none; + width: 0.5rem; + height: 0.5rem; + margin: 0 0.45rem 0 0.1rem; + border-top: 2px solid var(--signal); + border-right: 2px solid var(--signal); + transform: rotate(45deg); +} + +.entry { margin: 0 0 1.5rem; } + +.entry--user { + display: flex; + align-items: baseline; + gap: 0.55rem; + margin: 2.4rem 0 1rem; + color: var(--ink); + line-height: 1.5; +} + +.entry--user .chevron { transform: rotate(45deg) translate(-1px, -2px); } + +.entry--user p { margin: 0; overflow-wrap: anywhere; } + +.entry--agent { padding-left: 1.4rem; } + +/* tool calls */ + +.trace { + margin: 0 0 0.75rem -1.4rem; + font-size: 0.8rem; + line-height: 1.5; + color: var(--graphite-2); +} + +.trace p { margin: 0; } + +.trace__call { display: flex; align-items: baseline; gap: 0.5rem; } + +.trace__tool { margin-left: 0.1rem; color: var(--link); font-weight: 700; } + +.trace__arg { overflow-wrap: anywhere; } + +.dot { + flex: none; + width: 0.5rem; + height: 0.5rem; + margin-left: 0.1rem; + border-radius: 50%; + background: var(--link); + transform: translateY(-0.06rem); +} + +.trace__out { display: flex; align-items: baseline; gap: 0.55rem; padding-left: 0.45rem; } + +.branch { + flex: none; + width: 0.6rem; + height: 0.55rem; + border-left: 1.5px solid var(--graphite-2); + border-bottom: 1.5px solid var(--graphite-2); + border-bottom-left-radius: 3px; + transform: translateY(-0.22rem); +} + +/* entrance: only answers the visitor asked for animate in */ + +@keyframes fade { from { opacity: 0; } to { opacity: 1; } } + +@keyframes blink { 50% { opacity: 0; } } + +@keyframes pulse { + 0%, 100% { box-shadow: 0 0 0 4px var(--signal-weak); } + 50% { box-shadow: 0 0 0 8px transparent; } +} + +.is-live .trace, +.is-live .unit { animation: fade 0.18s ease-out both; } + +.is-live .b-list li, +.is-live .b-choices li, +.is-live .b-timeline li, +.is-live .b-tiers__row, +.is-live .b-projects li { + animation: fade 0.3s ease-out both; + animation-delay: calc(var(--i, 0) * 50ms); +} + +.is-typing .b-p::after { + content: ''; + display: inline-block; + width: 0.55em; + height: 1.1em; + margin-left: 2px; + vertical-align: -0.2em; + background: var(--signal); + animation: blink 1s steps(1) infinite; +} + +/* the first screen assembles once, then stays still */ + +.welcome, +.transcript > .entry--agent:first-of-type { animation: fade 0.5s ease-out both; } + +.transcript > .entry--agent:first-of-type { animation-delay: 0.12s; } + +/* ---------- answer blocks ---------- */ + +.unit { min-width: 0; } + +.b-p { + margin: 0.4rem 0 1rem; + max-width: 78ch; + color: var(--ink); +} + +.b-p strong { color: var(--ink); font-weight: 700; } + +.b-p a, .b-note a, .pairs a { + color: var(--link); + text-underline-offset: 3px; +} + +.b-p code, .b-note code { + padding: 0.05em 0.4em; + background: var(--mist); + border-radius: 4px; + font-size: 0.92em; +} + +.b-note { + margin: 0.35rem 0 1rem; + max-width: 78ch; + color: var(--graphite-2); + font-size: 0.84em; + line-height: 1.6; +} + +.b-list { + list-style: none; + margin: 0.4rem 0 1rem; + padding: 0; + max-width: 80ch; +} + +.b-list li { position: relative; margin: 0.45rem 0; padding-left: 1.3rem; } + +.b-list li::before { + content: '-'; + position: absolute; + left: 0.1rem; + top: 0; + color: var(--signal); +} + +.b-choices { margin: 0.5rem 0 1rem; padding-left: 1.7rem; max-width: 78ch; } + +.b-choices li { margin: 0.45rem 0; padding-left: 0.2rem; } + +.b-choices li::marker { color: var(--amber); font-weight: 700; } + +.b-pre { + margin: 0.4rem 0 1rem; + font: inherit; + line-height: 1.6; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +/* key / value rows */ + +.pairs { + display: grid; + grid-template-columns: minmax(0, 9rem) minmax(0, 1fr); + gap: 0.4rem 1.25rem; + margin: 0.5rem 0 1rem; + max-width: 78ch; +} + +.pairs dt { color: var(--graphite-2); font-size: 0.85em; } + +.pairs dd { margin: 0; } + +.b-facts { margin: 0.9rem 0 0.4rem; max-width: 78ch; } + +.b-facts h3 { + margin: 0; + font-family: var(--font-display); + font-size: 1.05rem; + font-weight: 600; + letter-spacing: -0.01em; +} + +.b-facts .pairs { grid-template-columns: 1fr; gap: 0; margin: 0.2rem 0 0.8rem; } + +.b-facts dt { + margin-top: 0.7rem; + color: var(--ink); + font-size: 1em; + font-weight: 500; + line-height: 1.45; +} + +.b-facts dd { margin: 0.05rem 0 0; color: var(--graphite); font-size: 0.88em; } + +/* the route and the timeline */ + +.b-route { + display: flex; + flex-wrap: wrap; + gap: 0.5rem 0; + margin: 0.5rem 0 1.5rem; + padding: 0; + list-style: none; +} + +.b-route li { + position: relative; + display: flex; + flex-direction: column; + padding-right: 1.6rem; +} + +.b-route li:not(:last-child)::after { + content: ''; + position: absolute; + right: 0.4rem; + top: 0.85em; + width: 0.8rem; + border-top: 1px dashed var(--graphite-2); +} + +.b-route__place { color: var(--ink); font-weight: 700; font-size: 0.9em; line-height: 1.5; } + +.b-route__note { color: var(--graphite-2); font-size: 0.72em; } + +.b-timeline { list-style: none; margin: 1rem 0 1.25rem; padding: 0; } + +.b-timeline li { + display: grid; + grid-template-columns: 1.25rem minmax(0, 1fr); + column-gap: 1rem; +} + +.b-timeline__rail { position: relative; } + +.b-timeline__rail::before { + content: ''; + position: absolute; + left: 50%; + top: 0; + bottom: 0; + width: 1px; + background: var(--rule); + transform: translateX(-50%); +} + +.b-timeline li:first-child .b-timeline__rail::before { top: 0.7rem; } +.b-timeline li:last-child .b-timeline__rail::before { bottom: auto; height: 0.7rem; } + +.b-timeline__node { + position: absolute; + left: 50%; + top: 0.7rem; + z-index: 1; + width: 9px; + height: 9px; + background: var(--paper); + border: 1.5px solid var(--graphite); + border-radius: 50%; + transform: translate(-50%, -50%); +} + +.b-timeline .is-current .b-timeline__node { + background: var(--signal); + border-color: var(--signal); + box-shadow: 0 0 0 4px var(--signal-weak); + animation: pulse 2.6s ease-in-out infinite; +} + +.b-timeline__body { padding-bottom: 1.9rem; } +.b-timeline li:last-child .b-timeline__body { padding-bottom: 0; } + +.b-timeline p { margin: 0; } + +.b-timeline__when { color: var(--graphite-2); font-size: 0.78em; line-height: 1.6; } + +.b-timeline .is-current .b-timeline__when { color: var(--signal); } + +.b-timeline__who { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.2rem 0.8rem; + margin-top: 0.15rem; +} + +.b-timeline__who .open { + font-family: var(--font-display); + font-size: 1.3rem; + letter-spacing: -0.015em; +} + +.b-timeline__place { color: var(--graphite-2); font-size: 0.76em; } + +.b-timeline__title { margin-top: 0.15rem; color: var(--ink); font-weight: 700; font-size: 0.92em; } + +.b-timeline__sum { margin-top: 0.3rem; max-width: 68ch; color: var(--graphite); font-size: 0.92em; } + +/* links that run a command */ + +.open { + padding: 0; + background: none; + border: 0; + border-bottom: 1.5px solid var(--graphite-2); + color: var(--ink); + font-weight: 700; + text-align: left; + cursor: pointer; + transition: color 0.15s ease, border-color 0.15s ease; +} + +.open:hover { color: var(--signal); border-bottom-color: var(--signal); } + +/* the stack, by how it was used */ + +.b-tiers { margin: 0.75rem 0 0.5rem; } + +.b-tiers__row { + display: grid; + grid-template-columns: minmax(0, 12rem) minmax(0, 1fr); + gap: 0.4rem 1.5rem; + padding: 0.85rem 0; + border-top: 1px solid var(--rule); +} + +.b-tiers dt { color: var(--ink); font-weight: 700; font-size: 0.88em; line-height: 1.45; } + +.b-tiers dt span { display: block; margin-top: 0.15rem; color: var(--graphite-2); font-weight: 400; font-size: 0.88em; } + +.b-tiers dd { + display: flex; + align-items: flex-start; + align-content: flex-start; + flex-wrap: wrap; + gap: 0.25rem 0.5rem; + margin: 0; + font-size: 0.82em; + line-height: 1.8; +} + +.b-tiers dd span { + padding: 0 0.5rem; + background: var(--mist); + border: 1px solid var(--rule); + border-radius: 4px; +} + +.b-skills { list-style: none; margin: 0.5rem 0 0.8rem; padding: 0; max-width: 80ch; } + +.b-skills li { padding: 0.7rem 0; border-top: 1px solid var(--rule); } + +.b-skills li:first-child { padding-top: 0; border-top: 0; } + +.b-skills p { margin: 0; } + +.b-skills__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.2rem 1rem; } + +.b-skills__head strong { color: var(--ink); } + +.b-skills__head span { color: var(--signal); font-size: 0.78em; } + +.b-skills li p:last-child { margin-top: 0.15rem; color: var(--graphite); font-size: 0.92em; } + +/* projects */ + +.b-projects { margin: 0.5rem 0 0.5rem; } + +.b-projects section + section { margin-top: 1.6rem; } + +.b-projects h3 { + margin: 0 0 0.3rem; + font-family: var(--font-display); + font-size: 0.95rem; + font-weight: 600; + color: var(--graphite); +} + +.b-projects ul { list-style: none; margin: 0; padding: 0; } + +.b-projects li { padding: 0.8rem 0; border-top: 1px solid var(--rule); } + +.b-projects p { margin: 0; } + +.b-projects__head { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; } + +.b-projects__head span { flex: none; color: var(--graphite-2); font-size: 0.76em; } + +.b-projects li p:last-child { margin-top: 0.25rem; max-width: 74ch; color: var(--graphite); font-size: 0.9em; } + +/* one role or project in detail */ + +.b-detail { max-width: 80ch; margin: 0.25rem 0 1rem; } + +.b-detail h3 { + margin: 0 0 0.3rem; + font-family: var(--font-display); + font-weight: 600; + font-size: 1.45rem; + line-height: 1.2; + letter-spacing: -0.02em; +} + +.b-detail p { margin: 0 0 0.8rem; } + +.b-detail .b-note { margin: 0.1rem 0 0.8rem; } + +.b-detail__meta { + display: flex; + flex-wrap: wrap; + gap: 0.2rem 1.1rem; + color: var(--graphite-2); + font-size: 0.78em; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin: 0.9rem 0 0; + padding: 0; + list-style: none; +} + +.tags li { + padding: 0 0.55rem; + background: var(--mist); + border: 1px solid var(--rule); + border-radius: 4px; + font-size: 0.78em; + line-height: 1.8; +} + +.b-commands { + display: grid; + grid-template-columns: minmax(0, 13rem) minmax(0, 1fr); + gap: 0.35rem 1.25rem; + margin: 0.5rem 0 1rem; +} + +.b-commands dt { color: var(--signal); font-weight: 700; } + +.b-commands dd { margin: 0; color: var(--graphite); } + +.b-ending { + margin: 0.8rem 0; + padding: 0.15rem 0 0.15rem 1rem; + border-left: 3px solid var(--amber); +} + +.b-ending p { margin: 0; } + +.b-ending__title { + color: var(--amber); + font-family: var(--font-display); + font-weight: 600; + font-size: 1.2rem; + letter-spacing: -0.015em; +} + +/* ---------- composer ---------- */ + +.composer { + position: sticky; + bottom: 0; + z-index: 5; + padding: 0.8rem 0 calc(0.9rem + env(safe-area-inset-bottom)); + background: var(--paper); + border-top: 1px solid var(--rule); +} + +.chips { + display: flex; + gap: 0.5rem; + min-height: 3.1rem; + padding: 0.1rem 0 0.7rem; + overflow-x: auto; + overscroll-behavior-x: contain; + scrollbar-width: none; +} + +.chips::-webkit-scrollbar { display: none; } + +.chip { + flex: none; + padding: 0.35rem 0.8rem; + background: transparent; + border: 1px solid var(--rule); + border-radius: 6px; + color: var(--graphite); + font-family: var(--font-mono); + font-size: 0.78rem; + line-height: 1.4; + white-space: nowrap; + cursor: pointer; + transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease; +} + +.chip:hover { border-color: var(--signal); color: var(--signal); background: var(--signal-weak); } + +.chip--skip { border-style: dashed; } + +.prompt-wrap { position: relative; } + +.prompt { + display: flex; + align-items: center; + gap: 0.3rem; + padding: 0.3rem 0.35rem 0.3rem 0.85rem; + background: var(--paper-2); + border: 1px solid var(--rule); + border-radius: 10px; + box-shadow: inset 0 1px 0 var(--sheen); + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +.prompt:focus-within { + border-color: var(--signal); + box-shadow: 0 0 0 3px var(--signal-weak); +} + +.prompt input { + flex: 1; + min-width: 0; + padding: 0.55rem 0; + background: transparent; + border: 0; + outline: none; + caret-color: var(--signal); + caret-shape: block; + color: var(--ink); + font-family: var(--font-mono); + font-size: 16px; +} + +.prompt input::placeholder { color: var(--graphite-2); opacity: 1; } + +.send { + padding: 0.5rem 0.95rem; + background: var(--signal); + border: 0; + border-radius: 7px; + color: var(--paper); + font-family: var(--font-mono); + font-size: 0.78rem; + font-weight: 700; + cursor: pointer; + transition: filter 0.15s ease; +} + +.send:hover { filter: brightness(1.12); } + +.menu { + position: absolute; + right: 0; + bottom: calc(100% + 0.4rem); + left: 0; + max-height: 40vh; + margin: 0; + padding: 0.3rem; + overflow-y: auto; + background: var(--paper-2); + border: 1px solid var(--rule); + border-radius: 10px; + box-shadow: 0 14px 36px rgba(0, 0, 0, 0.45); +} + +.menu [role='option'] { + display: grid; + grid-template-columns: minmax(0, 15rem) minmax(0, 1fr); + gap: 0.3rem 1rem; + padding: 0.4rem 0.6rem; + border-radius: 6px; + cursor: pointer; +} + +.menu [role='option'].is-active { background: var(--signal-weak); } + +.menu__name { color: var(--signal); font-size: 0.82rem; font-weight: 700; } + +.menu__hint { color: var(--graphite); font-size: 0.8rem; } + +/* ---------- plain page ---------- */ .wrap { width: 100%; @@ -72,56 +844,15 @@ a { color: inherit; } position: sticky; top: 2rem; align-self: start; + margin: 0; font-family: var(--font-mono); - font-size: 0.72rem; - letter-spacing: 0.22em; - text-transform: uppercase; + font-size: 0.78rem; + font-weight: 400; color: var(--graphite-2); } -/* ---------- top bar ---------- */ - -.topbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 1.75rem 0; - font-family: var(--font-mono); - font-size: 0.74rem; - letter-spacing: 0.08em; -} - -.topbar__name { color: var(--ink); text-transform: uppercase; } - -.topbar__place { - color: var(--graphite); - text-transform: uppercase; - letter-spacing: 0.16em; -} - -@keyframes pulse { - 0%, 100% { box-shadow: 0 0 0 4px var(--signal-weak); } - 50% { box-shadow: 0 0 0 7px rgba(31, 68, 240, 0.03); } -} - -/* ---------- hero ---------- */ - .hero { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: clamp(2rem, 5vw, 4rem); - align-items: center; - padding: clamp(2rem, 5vw, 3.5rem) 0 clamp(3.5rem, 7vw, 5.5rem); -} - -.eyebrow { - font-family: var(--font-mono); - font-size: 0.74rem; - letter-spacing: 0.2em; - text-transform: uppercase; - color: var(--graphite-2); - margin: 0 0 1.5rem; + padding: clamp(1.5rem, 4vw, 2.5rem) 0 clamp(3.5rem, 7vw, 5.5rem); } .hero__name { @@ -159,7 +890,8 @@ a { color: inherit; } margin: 2.25rem 0 0; } -.actions a { +.actions a, +.contact__links a { font-weight: 500; color: var(--ink); text-decoration: none; @@ -168,22 +900,8 @@ a { color: inherit; } transition: color 0.15s ease, border-color 0.15s ease; } -.actions a:hover { color: var(--signal); border-color: var(--signal); } - -.portrait { - width: 200px; - max-width: 100%; -} - -.portrait img { - width: 100%; - aspect-ratio: 4 / 5; - object-fit: cover; - border-radius: 6px; - border: 1px solid var(--mist); -} - -/* ---------- profile ---------- */ +.actions a:hover, +.contact__links a:hover { color: var(--signal); border-color: var(--signal); } .prose p { margin: 0 0 1.25rem; @@ -194,20 +912,16 @@ a { color: inherit; } .prose p:last-child { margin-bottom: 0; } -.prose strong { color: var(--ink); font-weight: 600; } - -/* ---------- experience timeline (the signature) ---------- */ - .experience__route { font-family: var(--font-mono); - font-size: 0.72rem; - letter-spacing: 0.05em; + font-size: 0.74rem; + letter-spacing: 0.02em; line-height: 1.9; color: var(--graphite-2); margin: 0 0 2.5rem; } -.roles { list-style: none; margin: 0; padding: 0; } +.roles { list-style: none; margin: 0 0 2rem; padding: 0; } .role { display: grid; @@ -256,8 +970,8 @@ a { color: inherit; } .role__year { font-family: var(--font-mono); - font-size: 0.72rem; - letter-spacing: 0.08em; + font-size: 0.74rem; + letter-spacing: 0.04em; color: var(--graphite-2); margin: 0 0 0.5rem; } @@ -281,8 +995,7 @@ a { color: inherit; } .role__loc { font-family: var(--font-mono); - font-size: 0.72rem; - letter-spacing: 0.06em; + font-size: 0.74rem; color: var(--graphite-2); } @@ -298,8 +1011,6 @@ a { color: inherit; } max-width: 58ch; } -/* ---------- stack ---------- */ - .stack { display: flex; flex-direction: column; gap: 1.6rem; } .group { @@ -314,14 +1025,14 @@ a { color: inherit; } .stack .group:last-child { border-bottom: 0; padding-bottom: 0; } .group__label { + margin: 0; font-family: var(--font-mono); - font-size: 0.72rem; - letter-spacing: 0.14em; - text-transform: uppercase; + font-size: 0.76rem; color: var(--graphite-2); } .group__tags { + margin: 0; font-family: var(--font-mono); font-size: 0.82rem; line-height: 1.95; @@ -330,8 +1041,6 @@ a { color: inherit; } .group__tags .t { color: var(--ink); } -/* ---------- contact ---------- */ - .contact__intro { color: var(--graphite); max-width: 50ch; @@ -359,19 +1068,6 @@ a { color: inherit; } margin: 2rem 0 0; } -.contact__links a { - font-weight: 500; - color: var(--ink); - text-decoration: none; - border-bottom: 1.5px solid var(--ink); - padding-bottom: 2px; - transition: color 0.15s ease, border-color 0.15s ease; -} - -.contact__links a:hover { color: var(--signal); border-color: var(--signal); } - -/* ---------- colophon ---------- */ - .colophon { display: flex; flex-wrap: wrap; @@ -380,25 +1076,34 @@ a { color: inherit; } padding: 2.25rem 0 3.5rem; border-top: 1px solid var(--mist); font-family: var(--font-mono); - font-size: 0.7rem; - letter-spacing: 0.08em; - text-transform: uppercase; + font-size: 0.72rem; + letter-spacing: 0.04em; color: var(--graphite-2); } /* ---------- 404 ---------- */ -.notfound { padding: clamp(4rem, 12vw, 9rem) 0; } +.notfound { padding-block: clamp(4rem, 12vw, 9rem); } + +.notfound__cmd { + display: flex; + align-items: baseline; + gap: 0.55rem; + margin: 0 0 1.25rem; + font-family: var(--font-mono); + font-size: 0.92rem; +} .notfound h1 { font-family: var(--font-display); font-weight: 600; - font-size: clamp(2.5rem, 8vw, 5rem); + font-size: clamp(2.2rem, 7vw, 4rem); letter-spacing: -0.03em; + line-height: 1.1; margin: 0 0 1rem; } -.notfound p { color: var(--graphite); max-width: 40ch; } +.notfound p { color: var(--graphite); max-width: 44ch; } .notfound a { color: var(--ink); @@ -411,6 +1116,10 @@ a { color: inherit; } /* ---------- responsive ---------- */ +@media (min-width: 700px) { + .chips { flex-wrap: wrap; overflow: visible; min-height: 0; } +} + @media (max-width: 820px) { .section { grid-template-columns: 1fr; gap: 1.5rem; } .section__label { position: static; top: auto; } @@ -418,21 +1127,16 @@ a { color: inherit; } } @media (max-width: 680px) { - .hero { - grid-template-columns: 1fr; - gap: 2.5rem; - } - .portrait { justify-self: start; order: -1; width: 140px; } - .portrait img { aspect-ratio: 1 / 1; } + .b-tiers__row { grid-template-columns: 1fr; } + .pairs, .b-commands { grid-template-columns: 1fr; gap: 0.1rem; } + .pairs dd, .b-commands dd { margin-bottom: 0.6rem; } + .menu [role='option'] { grid-template-columns: 1fr; gap: 0; } } @media (max-width: 520px) { - .topbar { flex-wrap: wrap; gap: 0.4rem 1rem; } -} - -@media (max-width: 460px) { - .topbar { font-size: 0.68rem; letter-spacing: 0.05em; } - .topbar__place { letter-spacing: 0.12em; } + .welcome { padding: 1.1rem; } + .chrome__id span { display: none; } + .b-projects__head { flex-direction: column; gap: 0.1rem; } } /* ---------- motion ---------- */ diff --git a/src/components/Classic.jsx b/src/components/Classic.jsx new file mode 100644 index 0000000..0ffd348 --- /dev/null +++ b/src/components/Classic.jsx @@ -0,0 +1,163 @@ +import React from 'react' + +import { + CERTIFICATIONS, + EDUCATION, + IDENTITY, + LANGUAGES, + ROLES, + ROUTE, + SKILLS, + SUMMARY, + TIERS, +} from '../data/profile.mjs' + +const Groups = ({ groups }) => ( +
+ {groups.map((group) => ( +
+

{group.label}

+

+ {group.items.map((item, i) => ( + + {i > 0 && ' · '} + {item} + + ))} +

+
+ ))} +
+) + +const SKILL_GROUPS = TIERS.map((tier) => ({ + label: tier.label, + items: SKILLS.filter((s) => s.tier === tier.id && s.listed !== false).map( + (s) => s.name + ), +})).filter((g) => g.items.length) + +const CREDENTIAL_GROUPS = [ + { label: 'Education', items: EDUCATION.map((e) => e.title) }, + { + label: 'Certifications', + items: CERTIFICATIONS.map((c) => `${c.name} (${c.year})`), + }, + { + label: 'Languages', + items: LANGUAGES.map(([name, level]) => `${name}, ${level}`), + }, +] + +// The same facts as the terminal, laid out as a page. It is also what search engines and +// visitors without JavaScript get. +const Classic = () => ( +
+
+
+

{IDENTITY.name}

+

+ Data pipelines, data products and the automation that keeps them + running. +

+

+ + {IDENTITY.title} at {IDENTITY.employer} + + , based in {IDENTITY.location}, with {IDENTITY.experience} across data + engineering, ML infrastructure and DevOps. +

+ +
+
+ +
+

Profile

+
+ {SUMMARY.map((text) => ( +

{text}

+ ))} +
+
+ +
+

Experience

+
+

+ {ROUTE.map((stop) => stop.place).join(' → ')} +

+
    + {ROLES.map((role) => ( +
  1. + +
    +

    {role.period}

    +
    +

    {role.company}

    + {role.place} +
    +

    {role.title}

    +

    {role.summary}

    +
    +
  2. + ))} +
+
+
+ +
+

Stack

+ +
+ +
+

Credentials

+ +
+ +
+

Contact

+
+

+ Always happy to talk about data, ML and platform engineering. Email is + the fastest way to reach me. +

+ + {IDENTITY.email} + +
+ + LinkedIn + +
+
+
+ + +
+) + +export default Classic diff --git a/src/components/Shell.jsx b/src/components/Shell.jsx new file mode 100644 index 0000000..04b084b --- /dev/null +++ b/src/components/Shell.jsx @@ -0,0 +1,112 @@ +import React, { useCallback, useEffect, useState } from 'react' + +import { IDENTITY } from '../data/profile.mjs' +import Classic from './Classic' +import Terminal from './Terminal' + +// Dark is the design; light is an opt-in that is remembered between visits. +const readTheme = () => { + try { + return window.localStorage.getItem('theme') === 'light' ? 'light' : 'dark' + } catch (e) { + return 'dark' + } +} + +const applyTheme = (theme) => { + const root = document.documentElement + if (theme === 'light') root.setAttribute('data-theme', 'light') + else root.removeAttribute('data-theme') + try { + if (theme === 'light') window.localStorage.setItem('theme', 'light') + else window.localStorage.removeItem('theme') + } catch (e) { + // Storage can be blocked; the theme then simply lasts for this visit. + } +} + +// The page frame: a header, the terminal, and the plain view behind a toggle. Both views +// stay mounted so switching back keeps the conversation. +const Shell = () => { + const [view, setView] = useState('terminal') + const [theme, setTheme] = useState('dark') + + useEffect(() => { + setTheme(readTheme()) + if (window.location.hash === '#classic') setView('classic') + }, []) + + const chooseTheme = useCallback((next) => { + applyTheme(next) + setTheme(next) + }, []) + + const toggleTheme = useCallback(() => { + chooseTheme(theme === 'light' ? 'dark' : 'light') + }, [theme, chooseTheme]) + + const showView = useCallback((next) => { + setView(next) + try { + window.history.replaceState( + null, + '', + next === 'classic' ? '#classic' : window.location.pathname + ) + } catch (e) { + // Rewriting the hash is optional. + } + window.scrollTo(0, 0) + }, []) + + const nav = useCallback( + (action) => { + if (action.type === 'classic') showView('classic') + if (action.type === 'theme') { + if (action.value === 'toggle') toggleTheme() + else chooseTheme(action.value) + } + }, + [showView, toggleTheme, chooseTheme] + ) + + return ( +
+
+

+ {IDENTITY.name} + {IDENTITY.location} +

+ +
+ +
+ + +
+
+ ) +} + +export default Shell diff --git a/src/components/Terminal.jsx b/src/components/Terminal.jsx new file mode 100644 index 0000000..793c949 --- /dev/null +++ b/src/components/Terminal.jsx @@ -0,0 +1,499 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' + +import { + answerToText, + completions, + INTRO, + respond, + STARTERS, + WELCOME, +} from '../agent/engine.mjs' +import { plainText } from '../agent/inline.mjs' +import { Block } from './blocks' + +const TICK = 16 +const DWELL_TRACE = 240 +const DWELL_BLOCK = 150 + +const prefersReducedMotion = () => + typeof window !== 'undefined' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches + +const isTouch = () => + typeof window !== 'undefined' && + window.matchMedia('(pointer: coarse)').matches + +const Welcome = () => ( +
+

{WELCOME.boot}

+

{WELCOME.headline}

+ {WELCOME.lines.map((line) => ( +

{line}

+ ))} +

{WELCOME.hint}

+
+) + +const UserEntry = ({ text }) => ( +
+
+) + +// One answer. While `animate` is set it reveals its tool calls and blocks one at a time and +// types out paragraphs. A skip, or reduced motion, shows everything at once. +const AgentEntry = ({ entry, skipToken, onDone, onTick, run, nav }) => { + const units = useMemo( + () => [ + ...entry.trace.map((t) => ({ kind: 'trace', t })), + ...entry.blocks.map((b) => ({ kind: 'block', b })), + ], + [entry] + ) + const [step, setStep] = useState(entry.animate ? 0 : units.length) + const [typed, setTyped] = useState({ step: -1, n: 0 }) + const done = step >= units.length + + const lastSkip = useRef(skipToken) + useEffect(() => { + if (skipToken !== lastSkip.current) { + lastSkip.current = skipToken + setStep(units.length) + } + }, [skipToken, units.length]) + + useEffect(() => { + if (done) return undefined + const unit = units[step] + let timer + if (unit.kind === 'block' && unit.b.t === 'p') { + const length = plainText(unit.b.text).length + const perTick = Math.max(2, Math.ceil(length / 90)) + let n = 0 + timer = setInterval(() => { + n += perTick + setTyped({ step, n }) + onTick() + if (n >= length) { + clearInterval(timer) + timer = setTimeout(() => setStep((s) => s + 1), DWELL_BLOCK) + } + }, TICK) + } else { + onTick() + timer = setTimeout( + () => setStep((s) => s + 1), + unit.kind === 'trace' ? DWELL_TRACE : DWELL_BLOCK + ) + } + return () => { + clearInterval(timer) + clearTimeout(timer) + } + }, [step, done, units, onTick]) + + const finished = useRef(false) + useEffect(() => { + if (done && entry.animate && !finished.current) { + finished.current = true + onDone(entry) + } + }, [done, entry, onDone]) + + const shown = done ? units.length : step + 1 + return ( +
+ {units.slice(0, shown).map((unit, i) => { + const current = !done && i === step + if (unit.kind === 'trace') { + return ( +
+

+

+

+

+
+ ) + } + const typing = current && unit.b.t === 'p' + const limit = typing ? (typed.step === step ? typed.n : 0) : undefined + return ( +
+ +
+ ) + })} +
+ ) +} + +const Terminal = ({ nav, active }) => { + const [entries, setEntries] = useState(() => [ + { id: 0, kind: 'welcome' }, + { id: 1, kind: 'agent', ...INTRO, animate: false }, + ]) + const [followups, setFollowups] = useState(INTRO.followups) + const [scene, setScene] = useState(null) + const [busy, setBusy] = useState(false) + const [skipToken, setSkipToken] = useState(0) + const [value, setValue] = useState('') + const [focused, setFocused] = useState(false) + const [menuIndex, setMenuIndex] = useState(0) + const [announce, setAnnounce] = useState('') + + const inputRef = useRef(null) + const nextId = useRef(2) + const sceneRef = useRef(null) + const navRef = useRef(nav) + const activeRef = useRef(active) + const history = useRef({ list: [], at: -1, draft: '' }) + const stick = useRef(true) + + navRef.current = nav + activeRef.current = active + + // Recalling an earlier command from history should not open the command menu over it. + const [recalling, setRecalling] = useState(false) + const items = useMemo( + () => + focused && !recalling + ? completions(value).filter((c) => c.insert !== value) + : [], + [focused, recalling, value] + ) + + // Follow the output while the visitor has not scrolled away from the bottom. + useEffect(() => { + const onScroll = () => { + stick.current = + window.innerHeight + window.scrollY >= + document.documentElement.scrollHeight - 160 + } + window.addEventListener('scroll', onScroll, { passive: true }) + return () => window.removeEventListener('scroll', onScroll) + }, []) + + const scrollToEnd = useCallback(() => { + if (!stick.current || !activeRef.current) return + window.requestAnimationFrame(() => + window.scrollTo(0, document.documentElement.scrollHeight) + ) + }, []) + + const focusInput = useCallback(() => { + if (!isTouch() && inputRef.current) + inputRef.current.focus({ preventScroll: true }) + }, []) + + const syncHash = useCallback((input) => { + try { + const keep = + input.startsWith('/') && + !/^\/(clear|cls|theme|classic|plain|cv|text|exit|quit|stop|q)\b/i.test( + input + ) + const url = + window.location.pathname + + window.location.search + + (keep ? `#${input.replace(/\s+/g, '%20')}` : '') + window.history.replaceState(null, '', url) + } catch (e) { + // The hash is a convenience; ignore browsers that refuse to rewrite it. + } + }, []) + + // A skipped answer reports back after the next question may already have started. + const latest = useRef(1) + const finish = useCallback((entry) => { + if (entry.id !== latest.current) return + setBusy(false) + setFollowups(entry.followups) + setAnnounce(answerToText(entry).slice(0, 1800)) + }, []) + + const run = useCallback( + (raw, echo) => { + const input = String(raw).trim() + if (!input) return + setSkipToken((t) => t + 1) + latest.current = -1 + + const inScene = sceneRef.current !== null + const out = respond(input, sceneRef.current) + sceneRef.current = out.scene + setScene(out.scene) + stick.current = true + + if (out.action) { + if (out.action.type === 'clear') { + latest.current = -1 + setEntries([]) + setFollowups(STARTERS) + setBusy(false) + return + } + if (out.action.type === 'theme') + navRef.current({ type: 'theme', value: out.action.value }) + if (out.action.type === 'classic') navRef.current({ type: 'classic' }) + } + + const h = history.current + // A numbered game choice is not worth recalling later. + const isChoice = inScene && !input.startsWith('/') + if (!isChoice && h.list[h.list.length - 1] !== input) h.list.push(input) + h.at = -1 + if (!out.action) syncHash(input) + + const animate = !prefersReducedMotion() + const id = nextId.current + nextId.current += 2 + const added = [{ id, kind: 'user', text: echo || input }] + if (out.answer) { + const entry = { id: id + 1, kind: 'agent', ...out.answer, animate } + latest.current = entry.id + added.push(entry) + if (animate) { + setBusy(true) + setFollowups([]) + } else { + finish(entry) + } + } + setEntries((prev) => [...prev, ...added]) + }, + [finish, syncHash] + ) + + const submit = useCallback(() => { + const text = value + setValue('') + setMenuIndex(0) + setRecalling(false) + run(text) + }, [run, value]) + + // Deep links: thiessen.io/#/projects opens that answer on load. + const booted = useRef(false) + useEffect(() => { + const open = () => { + const hash = decodeURIComponent(window.location.hash.slice(1)) + if (hash.startsWith('/')) run(hash) + } + if (!booted.current) { + booted.current = true + open() + } + window.addEventListener('hashchange', open) + return () => window.removeEventListener('hashchange', open) + }, [run]) + + useEffect(() => { + if (active) focusInput() + }, [active, focusInput]) + + // Follow new output, but leave the first screen where it loads. + const mounted = useRef(false) + useEffect(() => { + if (mounted.current) scrollToEnd() + mounted.current = true + }, [entries, followups, scrollToEnd]) + + const applyCompletion = (item) => { + setValue(item.insert) + setMenuIndex(0) + } + + const onKeyDown = (e) => { + if (busy && (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ')) + setSkipToken((t) => t + 1) + const h = history.current + if (e.key === 'Tab' && items.length) { + e.preventDefault() + applyCompletion(items[menuIndex] || items[0]) + } else if (e.key === 'ArrowUp') { + e.preventDefault() + if (items.length) { + setMenuIndex((i) => (i - 1 + items.length) % items.length) + } else if (h.list.length) { + if (h.at === -1) h.draft = value + h.at = h.at === -1 ? h.list.length - 1 : Math.max(0, h.at - 1) + setValue(h.list[h.at]) + setRecalling(true) + } + } else if (e.key === 'ArrowDown') { + e.preventDefault() + if (items.length) { + setMenuIndex((i) => (i + 1) % items.length) + } else if (h.at !== -1) { + h.at += 1 + if (h.at >= h.list.length) { + h.at = -1 + setValue(h.draft) + setRecalling(false) + } else { + setValue(h.list[h.at]) + } + } + } else if (e.key === 'Escape') { + if (items.length || value) { + setValue('') + setRecalling(false) + } + } else if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'l') { + e.preventDefault() + run('/clear') + } + } + + const placeholder = scene + ? 'Pick an option, or type /exit' + : 'Ask about Dennis, or type /' + + return ( +
{ + const selection = window.getSelection + ? window.getSelection().toString() + : '' + if ( + !selection && + !e.target.closest('a, button, input, [role="option"]') + ) + focusInput() + }} + > +
+ {entries.map((entry) => { + if (entry.kind === 'welcome') return + if (entry.kind === 'user') + return + return ( + + ) + })} +
+ +
+ {announce} +
+ +
{ + e.preventDefault() + submit() + }} + > +
+ {busy ? ( + + ) : ( + followups.map((f) => ( + + )) + )} +
+ +
+ {items.length > 0 && ( +
+ {items.map((item, i) => ( +
{ + e.preventDefault() + applyCompletion(item) + }} + > + {item.label} + {item.hint} +
+ ))} +
+ )} + +
+
+
+
+
+ ) +} + +export default Terminal diff --git a/src/components/blocks.jsx b/src/components/blocks.jsx new file mode 100644 index 0000000..158e67f --- /dev/null +++ b/src/components/blocks.jsx @@ -0,0 +1,264 @@ +import React from 'react' + +import { isSafeHref, parseInline, takeChars } from '../agent/inline.mjs' + +const isExternal = (href) => /^https?:/.test(href) + +// Renders answer text with its inline markup. `limit` cuts it for the typing effect. +export const Inline = ({ text, limit, nav }) => { + let segments = parseInline(text) + if (limit !== undefined) segments = takeChars(segments, limit) + return segments.map((seg, i) => { + if (seg.k === 'strong') return {seg.v} + if (seg.k === 'code') return {seg.v} + if (seg.k === 'link') { + if (seg.href === '#classic') { + return ( + { + e.preventDefault() + nav({ type: 'classic' }) + }} + > + {seg.v} + + ) + } + // Only known-safe targets become links; anything else stays plain text. + if (!isSafeHref(seg.href)) + return {seg.v} + return ( + + {seg.v} + + ) + } + return {seg.v} + }) +} + +const Tags = ({ items }) => + items && items.length ? ( + + ) : null + +const Pairs = ({ rows, nav }) => ( +
+ {rows.map(([key, value]) => ( + +
{key}
+
+ +
+
+ ))} +
+) + +const Open = ({ run, cmd, children }) => ( + +) + +// One answer block. `limit` is only used by paragraphs while they are being typed. +export const Block = ({ block, limit, run, nav }) => { + switch (block.t) { + case 'p': + return ( +

+ +

+ ) + case 'note': + return ( +

+ +

+ ) + case 'list': + return ( + + ) + case 'choices': + return ( +
    + {block.items.map((item, i) => ( +
  1. + {item} +
  2. + ))} +
+ ) + case 'kv': + return + case 'facts': + return ( +
+

{block.heading}

+ +
+ ) + case 'pre': + return
{block.lines.join('\n')}
+ case 'route': + return ( +
    + {block.stops.map((stop, i) => ( +
  1. + {stop.place} + {stop.note} +
  2. + ))} +
+ ) + case 'timeline': + return ( +
    + {block.items.map((role, i) => ( +
  1. +
  2. + ))} +
+ ) + case 'tiers': + return ( +
+ {block.groups.map((group, i) => ( +
+
+ {group.label} + {group.hint} +
+
+ {group.items.map((item) => ( + {item} + ))} +
+
+ ))} +
+ ) + case 'skills': + return ( + + ) + case 'projects': + return ( +
+ {block.groups.map((group) => ( +
+

{group.label}

+
    + {group.items.map((item, i) => ( +
  • +

    + + {item.name} + + {item.period} +

    +

    {item.blurb}

    +
  • + ))} +
+
+ ))} +
+ ) + case 'detail': + return ( +
+

{block.title}

+

+ {block.meta.map((part) => ( + {part} + ))} +

+ {block.note &&

{block.note}

} + {block.body && block.body.map((text, i) =>

{text}

)} + {block.points && block.points.length > 0 && ( + + )} + {block.scope &&

{block.scope}

} + +
+ ) + case 'commands': + return ( +
+ {block.rows.map((row) => ( + +
{row.usage}
+
{row.hint}
+
+ ))} +
+ ) + case 'ending': + return ( +
+

{block.title}

+

{block.text}

+
+ ) + default: + return null + } +} diff --git a/src/components/layout.js b/src/components/layout.js deleted file mode 100644 index 5481c91..0000000 --- a/src/components/layout.js +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'react' -import '../assets/style.css' - -const Layout = ({ children }) => <>{children} - -export default Layout diff --git a/src/data/profile.mjs b/src/data/profile.mjs new file mode 100644 index 0000000..2cbf674 --- /dev/null +++ b/src/data/profile.mjs @@ -0,0 +1,650 @@ +// Everything the site says about Dennis lives here. The terminal, the plain view and +// the tests all read this file, so a fact is written once. +// +// The page is written for the people who scan it fastest: recruiters and hiring managers. +// They look for the current role and title, the headline stack, what was delivered at each +// employer and at what scope, a line on education, and a way to get in touch. Anything +// personal, private or employer-internal stays out. +// +// Wording follows the verified claims in claude-resume-kit: verbs are scoped to what he did +// himself, the legacy warehouse work is in the past tense, skills carry the tier they were +// really used at, and nothing is quantified without evidence. + +export const IDENTITY = { + name: 'Dennis Thiessen', + first: 'Dennis', + title: 'Staff Data, Analytics & AI Engineer', + employer: 'Swisscom', + location: 'Bern, Switzerland', + email: 'dennis@thiessen.io', + linkedin: 'linkedin.com/in/dennis-thiessen', + linkedinUrl: 'https://www.linkedin.com/in/dennis-thiessen', + experience: '10+ years', +} + +export const SUMMARY = [ + 'Dennis Thiessen is a Staff Data, Analytics & AI Engineer at Swisscom in Bern, with more than ten years of experience where software engineering meets operations: data pipelines, ML inference in production, and the automation that keeps both running.', + 'Today he builds and models data products on Swisscom’s AWS data platform, in Python and PySpark. Before that he integrated ML inference into a 24/7 semiconductor fab at Bosch, and built research software and test automation in Hamburg and Bergen.', + 'What he cares about most is automation and reliability.', +] + +export const CURRENT = [ + 'At Swisscom he builds and models data products on the company’s AWS data platform: Python and PySpark jobs on Glue, Apache Iceberg tables queried through Athena, orchestrated with Airflow.', + 'He migrated pipelines in his own domains to AWS as part of the company-wide programme, writes unit tests (pytest) for the pipeline code, and defines data-quality alert thresholds for his pipelines. He also takes part in the team’s rotating engineer-on-duty role.', +] + +// Roles, most recent first. `live` marks the current one. +export const ROLES = [ + { + id: 'swisscom', + aliases: ['swisscom'], + period: '2023 to present', + company: 'Swisscom', + place: 'Bern, Switzerland', + title: 'Staff Data, Analytics & AI Engineer', + titleHistory: 'Senior from Oct 2023, Staff since Apr 2025', + live: true, + summary: + 'Builds and models data products on Swisscom’s AWS data platform, in Python and PySpark.', + points: [ + 'Now: Python and PySpark on AWS Glue, Apache Iceberg tables queried through Athena, orchestrated with Airflow, plus relational models and design for the data products.', + 'Migrated pipelines in his own domains to AWS as part of the company-wide programme.', + 'Unit tests (pytest) for pipeline code, and data-quality alert thresholds for his pipelines. Takes part in the team’s rotating engineer-on-duty role.', + 'Earlier, on the legacy warehouse: component owner for the ETL pipelines that moved business-critical data from Oracle into a Teradata warehouse, including data quality, root-cause analysis and on-call. Python applications there ran on Kubernetes and were deployed through automated GitLab CI/CD pipelines.', + 'Onboarded and mentored junior developers.', + ], + stack: [ + 'Python', + 'PySpark', + 'AWS Glue', + 'Apache Iceberg', + 'Athena', + 'Airflow', + 'SQL', + ], + }, + { + id: 'bosch', + aliases: ['bosch', 'semiconductor', 'dresden', 'fab'], + period: '2020 to 2022', + company: 'Bosch Semiconductor', + place: 'Dresden, Germany', + title: 'Senior Data Engineer, Data Analysis', + titleHistory: 'Data Engineer from Feb 2020, Senior from Jan 2021', + summary: + 'Integrated containerised ML inference into a 24/7 semiconductor fab and built the data services around it.', + points: [ + 'Devised and executed the integration of containerised, GPU-backed ML inference into the fab’s continuously running production environment, with Docker, Kubernetes and Ansible. A software defect could have halted production. The integration reduced the manual classification work for line engineers.', + 'Developed data services, mostly in Python with some Java and C#, over Oracle and Hadoop/Impala for internal analysis teams.', + 'Technical Application Owner for TIBCO Spotfire and the fab’s defect-management system, including SLOs, vendor contact and training. Co-presented at the TIBCO Analytics Forum 2022.', + 'Onboarded and mentored junior developers, and supervised students technically.', + ], + stack: [ + 'Python', + 'Docker', + 'Kubernetes', + 'Ansible', + 'Oracle', + 'Hadoop / Impala', + 'C#', + 'Java', + ], + }, + { + id: 'fraunhofer', + aliases: ['fraunhofer', 'cml', 'scedas'], + period: '2018 to 2019', + company: 'Fraunhofer CML', + place: 'Hamburg, Germany', + title: 'Research Software Engineer', + summary: + 'Developed crew-scheduling software and research microservices at the Fraunhofer Center for Maritime Logistics and Services.', + points: [ + 'SCEDAS crew-scheduling software in C#, Entity Framework and Microsoft SQL Server. Introduced Jenkins and built the jobs that deployed it automatically.', + 'Developed microservices for a research data-exchange platform with Express.js, Docker and SQLite.', + ], + stack: ['C#', 'Microsoft SQL Server', 'Jenkins', 'Express.js', 'Docker'], + }, + { + id: 'vizrt', + aliases: ['vizrt', 'bergen', 'norway', 'transcoding'], + period: '2017 to 2018', + company: 'Vizrt', + place: 'Bergen, Norway', + title: 'Test Automation / DevOps Engineer', + summary: + 'Contributed Python and C++ to a distributed video-transcoding backend, and wrote the automated tests that gated it in CI/CD.', + points: [ + 'Contributed Python and C++ to a distributed video-transcoding backend: a Python wrapper around a C++ transcoding engine built on FFmpeg.', + 'Developed automated test suites for audio, video and streaming, connected as quality gates in the CI/CD pipeline.', + ], + stack: ['Python', 'C++', 'CI/CD'], + }, + { + id: 'generali', + aliases: ['generali', 'rpa', 'uipath', 'bdd'], + period: '2015 to 2017', + company: 'Generali', + place: 'Hamburg, Germany', + title: 'IT Consultant', + titleHistory: + 'International Graduate Programme from May 2015, IT Consultant from Oct 2016', + summary: + 'Java/J2EE development and behaviour-driven testing at Generali’s IT services company.', + points: [ + 'Java/J2EE development on a process-oriented workflow portal: defect fixing, new requirements, and a contribution to design decisions for its task services.', + 'Introduced behaviour-driven development through a proof of concept, held technical responsibility for the UI test automation, and trained project members. Built RPA proofs of concept with UiPath.', + ], + stack: ['Java', 'J2EE', 'Selenium', 'Jenkins', 'UiPath'], + }, +] + +// Where the career took him, in order. +export const ROUTE = [ + { place: 'Munich', note: 'studies' }, + { place: 'Shanghai', note: 'thesis' }, + { place: 'Hamburg', note: '2015 to 2017' }, + { place: 'Bergen', note: '2017 to 2018' }, + { place: 'Hamburg', note: '2018 to 2019' }, + { place: 'Dresden', note: '2020 to 2022' }, + { place: 'Bern', note: '2023 to now' }, +] + +// Skills carry the tier they were really used at. `listed: false` keeps a skill out of the +// /stack overview but still answers a direct question about it honestly. +export const TIERS = [ + { id: 'now', label: 'Working with now', hint: 'Current production work' }, + { + id: 'before', + label: 'Used in production before', + hint: 'Earlier roles and the legacy warehouse', + }, + { id: 'ai', label: 'Hands-on with AI tooling', hint: 'Tools used directly' }, + { + id: 'poc', + label: 'Proof-of-concept scale', + hint: 'Built, never run as a platform', + }, + { + id: 'study', + label: 'From study and certification', + hint: 'Coursework, not production', + }, +] + +export const SKILLS = [ + { + name: 'Python', + tier: 'now', + aliases: ['python', 'python3'], + note: 'His main language at Swisscom and at Bosch.', + }, + { + name: 'SQL', + tier: 'now', + aliases: ['sql'], + note: 'Across Oracle, Teradata and Athena.', + }, + { + name: 'PySpark', + tier: 'now', + aliases: ['pyspark', 'spark'], + note: 'Current work at Swisscom.', + }, + { + name: 'AWS', + tier: 'now', + aliases: ['aws', 'amazon', 'glue', 's3', 'athena', 'redshift', 'cloud'], + note: 'S3, Glue, Athena, Redshift and Airflow at Swisscom. AWS Certified Solutions Architect – Associate, valid until Sep 2027.', + }, + { + name: 'Apache Iceberg', + tier: 'now', + aliases: ['iceberg'], + note: 'Part of the current Swisscom stack, queried through Athena.', + }, + { + name: 'Airflow', + tier: 'now', + aliases: ['airflow'], + note: 'Orchestrates the AWS pipelines at Swisscom.', + }, + { + name: 'CloudFormation', + tier: 'now', + aliases: ['cloudformation'], + note: 'Used for AWS infrastructure at Swisscom.', + }, + { + name: 'Docker', + tier: 'now', + aliases: [ + 'docker', + 'container', + 'containers', + 'containerised', + 'containerized', + ], + note: 'Production use at Bosch and Swisscom.', + }, + { + name: 'Data modelling', + tier: 'now', + aliases: ['modelling', 'modeling', 'datamodel', 'datamodelling'], + note: 'Logical and physical relational models for data products.', + }, + { + name: 'Unit testing (pytest)', + tier: 'now', + aliases: ['pytest', 'unittest', 'unittesting'], + note: 'Unit tests for pipeline code at Swisscom.', + }, + + { + name: 'Kafka', + tier: 'before', + aliases: ['kafka'], + note: 'Ingestion in the legacy warehouse unit at Swisscom, and the ELK proof of concept at Bosch.', + }, + { + name: 'Kubernetes', + tier: 'before', + aliases: ['kubernetes', 'k8s'], + note: 'Python applications at Swisscom (legacy unit) and ML inference at Bosch.', + }, + { + name: 'Ansible', + tier: 'before', + aliases: ['ansible'], + note: 'Used intensively at Bosch for configuration management, including his own extensions.', + }, + { + name: 'Teradata', + tier: 'before', + aliases: ['teradata', 'dwh', 'warehouse'], + note: 'The legacy data warehouse at Swisscom.', + }, + { + name: 'Oracle', + tier: 'before', + aliases: ['oracle'], + note: 'Source and analytics databases at Swisscom and Bosch.', + }, + { + name: 'Hadoop / Impala', + tier: 'before', + aliases: ['hadoop', 'impala'], + note: 'Data services at Bosch.', + }, + { + name: 'GitLab CI/CD', + tier: 'before', + aliases: ['gitlab', 'cicd'], + note: 'Deployed through automated pipelines at Swisscom (legacy unit).', + }, + { + name: 'Java', + tier: 'before', + aliases: ['java', 'j2ee'], + note: 'Bosch data services and the Generali workflow portal.', + }, + { + name: 'C#', + tier: 'before', + aliases: ['csharp', 'dotnet'], + note: 'Spotfire extensions and data services at Bosch, SCEDAS at Fraunhofer.', + }, + { + name: 'Linux and Bash', + tier: 'before', + aliases: ['linux', 'bash', 'shell'], + note: 'Server administration and automation at Bosch.', + }, + { + name: 'TIBCO Spotfire', + tier: 'before', + aliases: ['spotfire', 'tibco'], + note: 'Technical Application Owner and C# extension developer at Bosch.', + listed: false, + }, + { + name: 'Jenkins', + tier: 'before', + aliases: ['jenkins'], + note: 'Deployment jobs at Fraunhofer and BDD build jobs at Generali.', + listed: false, + }, + { + name: 'C++', + tier: 'before', + aliases: ['cpp'], + note: 'The transcoding engine at Vizrt, in a bounded role.', + listed: false, + }, + { + name: 'JavaScript', + tier: 'before', + aliases: ['javascript', 'node', 'nodejs', 'express'], + note: 'Express.js microservices at Fraunhofer, in a bounded role.', + listed: false, + }, + { + name: 'Kotlin', + tier: 'before', + aliases: ['kotlin'], + note: 'Some backend and Ansible-plugin work at Bosch.', + listed: false, + }, + { + name: 'Microsoft SQL Server', + tier: 'before', + aliases: ['mssql', 'sqlserver'], + note: 'Behind SCEDAS at Fraunhofer.', + listed: false, + }, + + { name: 'LiteLLM', tier: 'ai', aliases: ['litellm'], note: 'API use.' }, + { + name: 'Custom GPTs', + tier: 'ai', + aliases: ['gpt', 'gpts', 'chatgpt'], + note: 'Hands-on use.', + }, + { name: 'Copilot', tier: 'ai', aliases: ['copilot'], note: 'Hands-on use.' }, + { name: 'Kiro', tier: 'ai', aliases: ['kiro'], note: 'Hands-on use.' }, + + { + name: 'ELK Stack', + tier: 'poc', + aliases: ['elk', 'elasticsearch', 'kibana', 'logstash'], + note: 'Bosch proof of concept for log management and anomaly detection.', + listed: false, + }, + { + name: 'Grafana', + tier: 'poc', + aliases: ['grafana'], + note: 'Monitoring in the Bosch proof of concept.', + listed: false, + }, + { + name: 'Prometheus', + tier: 'poc', + aliases: ['prometheus'], + note: 'Monitoring in the Bosch proof of concept.', + listed: false, + }, + { + name: 'Loki', + tier: 'poc', + aliases: ['loki'], + note: 'Log aggregation in the Bosch proof of concept.', + listed: false, + }, + + { + name: 'TensorFlow / Keras', + tier: 'study', + aliases: ['tensorflow', 'keras'], + note: 'Covered in the IBM AI Engineering certificate (2020).', + listed: false, + }, + { + name: 'PyTorch', + tier: 'study', + aliases: ['pytorch', 'torch'], + note: 'Covered in the IBM AI Engineering certificate (2020).', + listed: false, + }, + { + name: 'Embedded systems', + tier: 'study', + aliases: ['embedded', 'vhdl', 'robotics'], + note: 'University coursework in C/C++ and VHDL, with no embedded work since.', + listed: false, + }, +] + +// Things that are not in his stack. A question about one gets a plain "not in his listed stack". +export const NON_CLAIMS = [ + ['TypeScript', ['typescript']], + ['Terraform', ['terraform']], + ['Azure', ['azure']], + ['GCP', ['gcp', 'googlecloud']], + ['Snowflake', ['snowflake']], + ['LangChain', ['langchain']], + ['LangGraph', ['langgraph']], + ['LlamaIndex', ['llamaindex']], + ['Model fine-tuning', ['finetuning', 'finetune']], +] + +// Selected work: the things worth a recruiter's two minutes. +export const PROJECTS = [ + { + id: 'data-products', + aliases: ['data products', 'data product', 'migration'], + kind: 'now', + name: 'Data products on Swisscom’s AWS platform', + period: 'Swisscom, current', + blurb: + 'Governed data products and migrated pipelines on a shared company platform.', + body: [ + 'Builds, models and documents data products on Swisscom’s company-wide AWS data platform: relational models, Python and PySpark jobs on Glue, Iceberg tables queried through Athena, Airflow for orchestration.', + 'He also migrated pipelines in his own domains from the legacy warehouse stack to AWS, as part of the wider company programme.', + ], + points: [ + 'Unit tests (pytest) for the pipeline code', + 'Alert thresholds, including data-quality checks, for his pipelines', + 'Part of the team’s rotating engineer-on-duty role', + ], + scope: + 'Scope: his own data products and pipelines on a platform the whole company builds on.', + stack: ['Python', 'PySpark', 'Glue', 'Iceberg', 'Athena', 'Airflow'], + }, + { + id: 'legacy-etl', + aliases: ['etl', 'legacy', 'data warehouse', 'legacy warehouse'], + kind: 'before', + name: 'Business-critical ETL into the legacy warehouse', + period: 'Swisscom, earlier', + blurb: + 'Component owner for the pipelines from Oracle into a Teradata warehouse.', + body: [ + 'Dennis owned the ETL pipelines that moved business-critical data from Oracle databases into the Teradata data warehouse and made it available for analysis. That covered data quality, security and compliance requirements, root-cause analysis, second and third-level support, and on-call.', + 'The Python applications around it ran on Kubernetes and were deployed through automated GitLab CI/CD pipelines. He also delivered dashboards and analyses for internal B2B stakeholders.', + ], + points: [], + scope: + 'Scope: component ownership for these pipelines, on a platform that was a team effort.', + stack: [ + 'Python', + 'Oracle', + 'Teradata', + 'Kafka', + 'Kubernetes', + 'GitLab CI/CD', + ], + }, + { + id: 'fab-inference', + aliases: ['inference', 'ml inference'], + kind: 'before', + name: 'ML inference in a 24/7 fab', + period: 'Bosch, 2020 to 2022', + blurb: + 'Containerised, GPU-backed ML inference integrated into a continuously running semiconductor fab.', + body: [ + 'At Bosch Dennis devised and executed how ML models were integrated into the fab’s production environment: containers on Kubernetes, configuration managed with Ansible, and the ingestion and inference scripts he designed.', + 'The inference automated image-based defect classification and reduced the manual classification work for line engineers. A software defect could have halted production.', + ], + points: ['Ran on NVIDIA GPUs', 'Docker, Kubernetes and Ansible end to end'], + scope: 'Scope: integration and operation of the inference.', + stack: ['Docker', 'Kubernetes', 'Ansible', 'Python', 'Linux'], + }, + { + id: 'fab-analytics', + aliases: [ + 'spotfire', + 'defect management', + 'analytics forum', + 'analytics applications', + ], + kind: 'before', + name: 'Analytics applications for the fab', + period: 'Bosch, 2020 to 2022', + blurb: + 'Technical owner of Spotfire and the defect-management system that fab analysts used.', + body: [ + 'Technical Application Owner for TIBCO Spotfire and the fab’s defect-management system: SLOs, vendor contact, training and documentation. He built Spotfire extensions in C# and co-presented at the TIBCO Analytics Forum 2022.', + 'He also developed the Python, Java and C# data services that fed the analysis teams from Oracle and Hadoop/Impala.', + ], + points: [], + scope: + 'Scope: technical Application Owner, and co-owner of the Spotfire environment.', + stack: ['TIBCO Spotfire', 'C#', 'Python', 'Oracle', 'Hadoop / Impala'], + }, +] + +export const KINDS = { + now: 'Current', + before: 'Earlier', +} + +export const EDUCATION = [ + { + id: 'meng', + title: 'M.Eng. Computer Aided Engineering', + detail: + 'Focus on software design and engineering. Universität der Bundeswehr München, 2013. Thesis written at Tongji University in Shanghai.', + }, + { + id: 'beng', + title: 'B.Eng. Information and Telecommunication Technologies', + detail: 'Universität der Bundeswehr München, 2012.', + }, +] + +export const CERTIFICATIONS = [ + { + name: 'AWS Certified Solutions Architect – Associate', + year: '2024', + note: 'valid until Sep 2027', + }, + { + name: 'Data Engineering with AWS', + year: '2026', + note: 'Udacity nanodegree', + }, + { + name: 'IBM AI Engineering Professional Certificate', + year: '2020', + note: 'Coursera', + }, + { + name: 'iSAQB Certified Professional for Software Architecture, Foundation Level', + year: '2016', + note: '', + }, +] + +export const LANGUAGES = [ + ['German', 'native'], + ['English', 'fluent'], +] + +// Hand-written answers for topics that cut across roles. Each value is a list of blocks. +export const TOPICS = { + ai: { + blocks: [ + { + t: 'p', + text: '**ML in production.** At Bosch (2020 to 2022) Dennis devised and executed the integration of ML models into a 24/7 semiconductor production environment: containerised, GPU-backed inference orchestrated with Docker, Kubernetes and Ansible. His scope was the integration and its operation around the models.', + }, + { + t: 'p', + text: '**LLM tooling.** At Swisscom he configured domain-grounded LLM assistants in a Swisscom-owned web interface by choosing models and supplying curated knowledge. He has hands-on experience with LiteLLM, custom GPTs, Copilot and Kiro.', + }, + { + t: 'p', + text: '**Study.** IBM AI Engineering Professional Certificate (2020).', + }, + ], + }, + mentoring: { + blocks: [ + { + t: 'list', + items: [ + 'Onboarded and mentored junior developers at Bosch and Swisscom. This is technical guidance, not line management.', + 'At Bosch: technical supervision of students.', + 'At Generali: introduced behaviour-driven development, presented it to the Java community and trained project members.', + ], + }, + ], + }, + reliability: { + blocks: [ + { + t: 'list', + items: [ + 'Today at Swisscom: Dennis takes part in the team’s rotating engineer-on-duty role and defines alert thresholds, including data-quality checks, for his own pipelines. Alerts turn into tickets that the team assigns and fixes.', + 'On the legacy warehouse: component owner for the Oracle-to-Teradata ETL, including root-cause analysis, second and third-level support, and on-call.', + 'At Bosch: Application Owner for analytics software, with SLOs. A software defect could have halted the fab.', + ], + }, + ], + }, + testing: { + blocks: [ + { + t: 'list', + items: [ + 'Swisscom: unit tests (pytest) for pipeline code.', + 'Vizrt: automated test suites for audio, video and streaming, wired into CI/CD as quality gates.', + 'Generali: introduced behaviour-driven development and held technical responsibility for UI test automation.', + ], + }, + ], + }, + work: { + blocks: [ + { + t: 'kv', + rows: [ + ['Based in', IDENTITY.location], + ['Languages', LANGUAGES.map(([l, v]) => `${l} (${v})`).join(', ')], + ], + }, + { + t: 'p', + text: 'For availability, location preferences or terms, the quickest answer is an email to Dennis.', + }, + ], + }, + site: { + blocks: [ + { + t: 'p', + text: 'Every answer comes from a curated knowledge base that ships with this page. There is no language model, and answering you needs no network request, so nothing here can be invented and nothing you type leaves your browser. The tool calls you see are real lookups against that data.', + }, + { + t: 'p', + text: 'A model-backed version is possible. For facts about a real person, this one trades flexibility for accuracy on purpose.', + }, + { + t: 'p', + text: 'Prefer plain text? Switch to the [classic view](#classic).', + }, + ], + }, +} + +export const WELCOME = { + boot: 'dt ~/thiessen.io retrieval-only', + headline: 'Ask about Dennis Thiessen', + lines: [ + 'I answer questions about his work, stack and projects.', + 'Every answer comes from a curated knowledge base. No model is running, so nothing is made up.', + ], + hint: 'Pick a suggestion, type a question, or type / for commands.', +} diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro new file mode 100644 index 0000000..bce1ec0 --- /dev/null +++ b/src/layouts/Base.astro @@ -0,0 +1,42 @@ +--- +// The document shell shared by every page: fonts, icons, theme and base styles. +// Fonts are served from this site (via @fontsource), so visitors make no request to +// Google or any other third party. +import '@fontsource-variable/hanken-grotesk' +import '@fontsource-variable/jetbrains-mono' +import '@fontsource-variable/space-grotesk' +import '../assets/style.css' + +interface Props { + title: string + description: string + noindex?: boolean +} + +const { title, description, noindex = false } = Astro.props + +// Dark is the design; a visitor who chose the light theme gets it before first paint, +// so they never see a flash of dark. +const themeScript = + "try{if(localStorage.getItem('theme')==='light')document.documentElement.setAttribute('data-theme','light')}catch(e){}" +--- + + + + + + + {title} + + {noindex && } + + + + +