chore: initial commit

This commit is contained in:
Thomas Bishop 2025-07-07 17:08:27 +01:00
commit dc23fac057
194 changed files with 13947 additions and 0 deletions

27
.gitignore vendored Normal file
View file

@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
public/posts
public/post-index.json

8
.prettierrc Normal file
View file

@ -0,0 +1,8 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": false,
"printWidth": 80,
"proseWrap": "always"
}

32
README.md Normal file
View file

@ -0,0 +1,32 @@
# Systems Obscure
> Another software engineer with a blog
![Screenshot of blog](./public/blog-screenshot.png)
## Build
```sh
npm run build
```
This runs `vite build` using [speedy web compiler](https://swc.rs/) for the
transpilation of TSX to JSX. I am using TypeScript so that I can use the
[shadcn](https://ui.shadcn.com/) component library however I don't actually care
about types in this project and all my components are written as JSX. SWC
transpiles very quickly without throwing TS warning and errors.
I still use Vite's native `esbuild` for HMR transpilation in development.
## Scripts
### Generate post index
```sh
npm run build:posts
```
This runs `scripts/generate-post-index.js` which reads all raw Markdown posts in
`/posts` and parses the body content and YAML front-matter. It then writes this
data to `public/post-index.json` which is read by the React application at
runtime.

21
components.json Normal file
View file

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

28
eslint.config.js Normal file
View file

@ -0,0 +1,28 @@
import js from "@eslint/js"
import globals from "globals"
import reactHooks from "eslint-plugin-react-hooks"
import reactRefresh from "eslint-plugin-react-refresh"
import tseslint from "typescript-eslint"
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
}
)

16
index.html Normal file
View file

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

6757
package-lock.json generated Normal file
View file

@ -0,0 +1,6757 @@
{
"name": "systems-obscure",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "systems-obscure",
"version": "0.0.0",
"dependencies": {
"@radix-ui/react-navigation-menu": "^1.2.12",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-toggle": "^1.1.8",
"@shikijs/colorized-brackets": "^3.6.0",
"@tailwindcss/vite": "^4.1.6",
"@vitejs/plugin-react-swc": "^3.10.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"gray-matter": "^4.0.3",
"i": "^0.3.7",
"lucide-react": "^0.509.0",
"marked": "^15.0.12",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.6.0",
"sharp": "^0.34.2",
"shiki": "^3.6.0",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.6"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@types/node": "^22.15.17",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"tw-animate-css": "^1.2.9",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/compat-data": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz",
"integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz",
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.27.1",
"@babel/helper-compilation-targets": "^7.27.1",
"@babel/helper-module-transforms": "^7.27.1",
"@babel/helpers": "^7.27.1",
"@babel/parser": "^7.27.1",
"@babel/template": "^7.27.1",
"@babel/traverse": "^7.27.1",
"@babel/types": "^7.27.1",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
"json5": "^2.2.3",
"semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/babel"
}
},
"node_modules/@babel/generator": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz",
"integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.27.1",
"@babel/types": "^7.27.1",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^3.0.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.27.2",
"@babel/helper-validator-option": "^7.27.1",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.27.1",
"@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz",
"integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1",
"@babel/traverse": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-plugin-utils": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-option": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz",
"integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.27.1",
"@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz",
"integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.27.1"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/plugin-transform-react-jsx-self": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-react-jsx-source": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
"integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/parser": "^7.27.2",
"@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz",
"integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.27.1",
"@babel/parser": "^7.27.1",
"@babel/template": "^7.27.1",
"@babel/types": "^7.27.1",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse/node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/types": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz",
"integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
"integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
"integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
"integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
"integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
"integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
"integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
"integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
"integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
"integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
"integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
"integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
"integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
"integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
"cpu": [
"mips64el"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
"integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
"integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
"integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
"integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
"integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
"integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
"integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
"integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
"integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
"integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
"integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
"integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
"dev": true,
"license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
},
"peerDependencies": {
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
}
},
"node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint-community/regexpp": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
"node_modules/@eslint/config-array": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
"integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/object-schema": "^2.1.6",
"debug": "^4.3.1",
"minimatch": "^3.1.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/config-helpers": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz",
"integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/core": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
"integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/eslintrc": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^10.0.1",
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/eslintrc/node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@eslint/js": {
"version": "9.26.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz",
"integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/object-schema": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.2.8",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz",
"integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.13.0",
"levn": "^0.4.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/node": {
"version": "0.16.6",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
"integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/core": "^0.19.1",
"@humanwhocodes/retry": "^0.3.0"
},
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
"integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=12.22"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@humanwhocodes/retry": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz",
"integrity": "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.1.0"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz",
"integrity": "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.1.0"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz",
"integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==",
"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.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz",
"integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==",
"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.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz",
"integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==",
"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.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz",
"integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==",
"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.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz",
"integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==",
"cpu": [
"ppc64"
],
"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.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz",
"integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==",
"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.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz",
"integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==",
"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.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz",
"integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==",
"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.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz",
"integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==",
"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.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz",
"integrity": "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.1.0"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz",
"integrity": "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.1.0"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz",
"integrity": "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==",
"cpu": [
"s390x"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.1.0"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz",
"integrity": "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.1.0"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz",
"integrity": "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.1.0"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz",
"integrity": "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.1.0"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz",
"integrity": "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.4.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz",
"integrity": "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz",
"integrity": "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz",
"integrity": "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
"license": "ISC",
"dependencies": {
"minipass": "^7.0.4"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.1.tgz",
"integrity": "sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"content-type": "^1.0.5",
"cors": "^2.8.5",
"cross-spawn": "^7.0.3",
"eventsource": "^3.0.2",
"express": "^5.0.1",
"express-rate-limit": "^7.5.0",
"pkce-challenge": "^5.0.0",
"raw-body": "^3.0.0",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.24.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.stat": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.walk": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz",
"integrity": "sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.2",
"@radix-ui/react-slot": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz",
"integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz",
"integrity": "sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.2",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-escape-keydown": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-navigation-menu": {
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.12.tgz",
"integrity": "sha512-iExvawdu7n6DidDJRU5pMTdi+Z3DaVPN4UZbAGuTs7nJA8P4RvvkEz+XYI2UJjb/Hh23RrH19DakgZNLdaq9Bw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-collection": "1.1.6",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.9",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.2",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-visually-hidden": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-presence": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
"integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-primitive": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz",
"integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz",
"integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.8.tgz",
"integrity": "sha512-hrpa59m3zDnsa35LrTOH5s/a3iGv/VD+KKQjjiCTo/W4r0XwPpiWQvAv6Xl1nupSoaZeNNxW6sJH9ZydsjKdYQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-primitive": "2.1.2",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-effect-event": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-previous": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-visually-hidden": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.2.tgz",
"integrity": "sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.11",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz",
"integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==",
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
"integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz",
"integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz",
"integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz",
"integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz",
"integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz",
"integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz",
"integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz",
"integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz",
"integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz",
"integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz",
"integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz",
"integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz",
"integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz",
"integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz",
"integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz",
"integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz",
"integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz",
"integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz",
"integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz",
"integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@shikijs/colorized-brackets": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@shikijs/colorized-brackets/-/colorized-brackets-3.6.0.tgz",
"integrity": "sha512-vYAj5trQvNXrxp5gmvBX3SMQj5vKC9etfGZc01hjxQbgldKjmvXGoA/no1U5tuaqpjQGx0TBPyNdASg6mfvVFg==",
"license": "MIT",
"dependencies": {
"shiki": "3.6.0"
}
},
"node_modules/@shikijs/core": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.6.0.tgz",
"integrity": "sha512-9By7Xb3olEX0o6UeJyPLI1PE1scC4d3wcVepvtv2xbuN9/IThYN4Wcwh24rcFeASzPam11MCq8yQpwwzCgSBRw==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.6.0",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4",
"hast-util-to-html": "^9.0.5"
}
},
"node_modules/@shikijs/engine-javascript": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.6.0.tgz",
"integrity": "sha512-7YnLhZG/TU05IHMG14QaLvTW/9WiK8SEYafceccHUSXs2Qr5vJibUwsDfXDLmRi0zHdzsxrGKpSX6hnqe0k8nA==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.6.0",
"@shikijs/vscode-textmate": "^10.0.2",
"oniguruma-to-es": "^4.3.3"
}
},
"node_modules/@shikijs/engine-oniguruma": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.6.0.tgz",
"integrity": "sha512-nmOhIZ9yT3Grd+2plmW/d8+vZ2pcQmo/UnVwXMUXAKTXdi+LK0S08Ancrz5tQQPkxvjBalpMW2aKvwXfelauvA==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.6.0",
"@shikijs/vscode-textmate": "^10.0.2"
}
},
"node_modules/@shikijs/langs": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.6.0.tgz",
"integrity": "sha512-IdZkQJaLBu1LCYCwkr30hNuSDfllOT8RWYVZK1tD2J03DkiagYKRxj/pDSl8Didml3xxuyzUjgtioInwEQM/TA==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.6.0"
}
},
"node_modules/@shikijs/themes": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.6.0.tgz",
"integrity": "sha512-Fq2j4nWr1DF4drvmhqKq8x5vVQ27VncF8XZMBuHuQMZvUSS3NBgpqfwz/FoGe36+W6PvniZ1yDlg2d4kmYDU6w==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.6.0"
}
},
"node_modules/@shikijs/types": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.6.0.tgz",
"integrity": "sha512-cLWFiToxYu0aAzJqhXTQsFiJRTFDAGl93IrMSBNaGSzs7ixkLfdG6pH11HipuWFGW5vyx4X47W8HDQ7eSrmBUg==",
"license": "MIT",
"dependencies": {
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
}
},
"node_modules/@shikijs/vscode-textmate": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
"license": "MIT"
},
"node_modules/@swc/core": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.7.tgz",
"integrity": "sha512-bcpllEihyUSnqp0UtXTvXc19CT4wp3tGWLENhWnjr4B5iEOkzqMu+xHGz1FI5IBatjfqOQb29tgIfv6IL05QaA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.23"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.12.7",
"@swc/core-darwin-x64": "1.12.7",
"@swc/core-linux-arm-gnueabihf": "1.12.7",
"@swc/core-linux-arm64-gnu": "1.12.7",
"@swc/core-linux-arm64-musl": "1.12.7",
"@swc/core-linux-x64-gnu": "1.12.7",
"@swc/core-linux-x64-musl": "1.12.7",
"@swc/core-win32-arm64-msvc": "1.12.7",
"@swc/core-win32-ia32-msvc": "1.12.7",
"@swc/core-win32-x64-msvc": "1.12.7"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.17"
},
"peerDependenciesMeta": {
"@swc/helpers": {
"optional": true
}
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.7.tgz",
"integrity": "sha512-w6BBT0hBRS56yS+LbReVym0h+iB7/PpCddqrn1ha94ra4rZ4R/A91A/rkv+LnQlPqU/+fhqdlXtCJU9mrhCBtA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.12.7.tgz",
"integrity": "sha512-jN6LhFfGOpm4DY2mXPgwH4aa9GLOwublwMVFFZ/bGnHYYCRitLZs9+JWBbyWs7MyGcA246Ew+EREx36KVEAxjA==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.12.7.tgz",
"integrity": "sha512-rHn8XXi7G2StEtZRAeJ6c7nhJPDnqsHXmeNrAaYwk8Tvpa6ZYG2nT9E1OQNXj1/dfbSFTjdiA8M8ZvGYBlpBoA==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.7.tgz",
"integrity": "sha512-N15hKizSSh+hkZ2x3TDVrxq0TDcbvDbkQJi2ZrLb9fK+NdFUV/x+XF16ZDPlbxtrGXl1CT7VD439SNaMN9F7qw==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.12.7.tgz",
"integrity": "sha512-jxyINtBezpxd3eIUDiDXv7UQ87YWlPsM9KumOwJk09FkFSO4oYxV2RT+Wu+Nt5tVWue4N0MdXT/p7SQsDEk4YA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.7.tgz",
"integrity": "sha512-PR4tPVwU1BQBfFDk2XfzXxsEIjF3x/bOV1BzZpYvrlkU0TKUDbR4t2wzvsYwD/coW7/yoQmlL70/qnuPtTp1Zw==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.7.tgz",
"integrity": "sha512-zy7JWfQtQItgMfUjSbbcS3DZqQUn2d9VuV0LSGpJxtTXwgzhRpF1S2Sj7cU9hGpbM27Y8RJ4DeFb3qbAufjbrw==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.12.7.tgz",
"integrity": "sha512-52PeF0tyX04ZFD8nibNhy/GjMFOZWTEWPmIB3wpD1vIJ1po+smtBnEdRRll5WIXITKoiND8AeHlBNBPqcsdcwA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.12.7.tgz",
"integrity": "sha512-WzQwkNMuhB1qQShT9uUgz/mX2j7NIEPExEtzvGsBT7TlZ9j1kGZ8NJcZH/fwOFcSJL4W7DnkL7nAhx6DBlSPaA==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.12.7",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.12.7.tgz",
"integrity": "sha512-R52ivBi2lgjl+Bd3XCPum0YfgbZq/W1AUExITysddP9ErsNSwnreYyNB3exEijiazWGcqHEas2ChiuMOP7NYrA==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"license": "Apache-2.0"
},
"node_modules/@swc/types": {
"version": "0.1.23",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.23.tgz",
"integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==",
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3"
}
},
"node_modules/@tailwindcss/node": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.6.tgz",
"integrity": "sha512-ed6zQbgmKsjsVvodAS1q1Ld2BolEuxJOSyyNc+vhkjdmfNUDCmQnlXBfQkHrlzNmslxHsQU/bFmzcEbv4xXsLg==",
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2",
"lightningcss": "1.29.2",
"magic-string": "^0.30.17",
"source-map-js": "^1.2.1",
"tailwindcss": "4.1.6"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.6.tgz",
"integrity": "sha512-0bpEBQiGx+227fW4G0fLQ8vuvyy5rsB1YIYNapTq3aRsJ9taF3f5cCaovDjN5pUGKKzcpMrZst/mhNaKAPOHOA==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.4",
"tar": "^7.4.3"
},
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.6",
"@tailwindcss/oxide-darwin-arm64": "4.1.6",
"@tailwindcss/oxide-darwin-x64": "4.1.6",
"@tailwindcss/oxide-freebsd-x64": "4.1.6",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.6",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.6",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.6",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.6",
"@tailwindcss/oxide-linux-x64-musl": "4.1.6",
"@tailwindcss/oxide-wasm32-wasi": "4.1.6",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.6",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.6"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.6.tgz",
"integrity": "sha512-VHwwPiwXtdIvOvqT/0/FLH/pizTVu78FOnI9jQo64kSAikFSZT7K4pjyzoDpSMaveJTGyAKvDjuhxJxKfmvjiQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.6.tgz",
"integrity": "sha512-weINOCcqv1HVBIGptNrk7c6lWgSFFiQMcCpKM4tnVi5x8OY2v1FrV76jwLukfT6pL1hyajc06tyVmZFYXoxvhQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.6.tgz",
"integrity": "sha512-3FzekhHG0ww1zQjQ1lPoq0wPrAIVXAbUkWdWM8u5BnYFZgb9ja5ejBqyTgjpo5mfy0hFOoMnMuVDI+7CXhXZaQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.6.tgz",
"integrity": "sha512-4m5F5lpkBZhVQJq53oe5XgJ+aFYWdrgkMwViHjRsES3KEu2m1udR21B1I77RUqie0ZYNscFzY1v9aDssMBZ/1w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.6.tgz",
"integrity": "sha512-qU0rHnA9P/ZoaDKouU1oGPxPWzDKtIfX7eOGi5jOWJKdxieUJdVV+CxWZOpDWlYTd4N3sFQvcnVLJWJ1cLP5TA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.6.tgz",
"integrity": "sha512-jXy3TSTrbfgyd3UxPQeXC3wm8DAgmigzar99Km9Sf6L2OFfn/k+u3VqmpgHQw5QNfCpPe43em6Q7V76Wx7ogIQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.6.tgz",
"integrity": "sha512-8kjivE5xW0qAQ9HX9reVFmZj3t+VmljDLVRJpVBEoTR+3bKMnvC7iLcoSGNIUJGOZy1mLVq7x/gerVg0T+IsYw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.6.tgz",
"integrity": "sha512-A4spQhwnWVpjWDLXnOW9PSinO2PTKJQNRmL/aIl2U/O+RARls8doDfs6R41+DAXK0ccacvRyDpR46aVQJJCoCg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.6.tgz",
"integrity": "sha512-YRee+6ZqdzgiQAHVSLfl3RYmqeeaWVCk796MhXhLQu2kJu2COHBkqlqsqKYx3p8Hmk5pGCQd2jTAoMWWFeyG2A==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.6.tgz",
"integrity": "sha512-qAp4ooTYrBQ5pk5jgg54/U1rCJ/9FLYOkkQ/nTE+bVMseMfB6O7J8zb19YTpWuu4UdfRf5zzOrNKfl6T64MNrQ==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
"@emnapi/runtime",
"@tybys/wasm-util",
"@emnapi/wasi-threads",
"tslib"
],
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@emnapi/wasi-threads": "^1.0.2",
"@napi-rs/wasm-runtime": "^0.2.9",
"@tybys/wasm-util": "^0.9.0",
"tslib": "^2.8.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.6.tgz",
"integrity": "sha512-nqpDWk0Xr8ELO/nfRUDjk1pc9wDJ3ObeDdNMHLaymc4PJBWj11gdPCWZFKSK2AVKjJQC7J2EfmSmf47GN7OuLg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.6.tgz",
"integrity": "sha512-5k9xF33xkfKpo9wCvYcegQ21VwIBU1/qEbYlVukfEIyQbEA47uK8AAwS7NVjNE3vHzcmxMYwd0l6L4pPjjm1rQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.6.tgz",
"integrity": "sha512-zjtqjDeY1w3g2beYQtrMAf51n5G7o+UwmyOjtsDMP7t6XyoRMOidcoKP32ps7AkNOHIXEOK0bhIC05dj8oJp4w==",
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.1.6",
"@tailwindcss/oxide": "4.1.6",
"tailwindcss": "4.1.6"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.20.7",
"@babel/types": "^7.20.7",
"@types/babel__generator": "*",
"@types/babel__template": "*",
"@types/babel__traverse": "*"
}
},
"node_modules/@types/babel__generator": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.0.0"
}
},
"node_modules/@types/babel__template": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.1.0",
"@babel/types": "^7.0.0"
}
},
"node_modules/@types/babel__traverse": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
"integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
"license": "MIT"
},
"node_modules/@types/hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
"license": "MIT",
"dependencies": {
"@types/unist": "*"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
},
"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/node": {
"version": "22.15.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz",
"integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/react": {
"version": "19.1.3",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz",
"integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.1.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.3.tgz",
"integrity": "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==",
"devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.0.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/@typescript-eslint/eslint-plugin": {
"version": "8.32.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz",
"integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.32.0",
"@typescript-eslint/type-utils": "8.32.0",
"@typescript-eslint/utils": "8.32.0",
"@typescript-eslint/visitor-keys": "8.32.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.32.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz",
"integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.32.0",
"@typescript-eslint/types": "8.32.0",
"@typescript-eslint/typescript-estree": "8.32.0",
"@typescript-eslint/visitor-keys": "8.32.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.32.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz",
"integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.32.0",
"@typescript-eslint/visitor-keys": "8.32.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.32.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz",
"integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.32.0",
"@typescript-eslint/utils": "8.32.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.32.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz",
"integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.32.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz",
"integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.32.0",
"@typescript-eslint/visitor-keys": "8.32.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.32.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz",
"integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.32.0",
"@typescript-eslint/types": "8.32.0",
"@typescript-eslint/typescript-estree": "8.32.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.32.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz",
"integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.32.0",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC"
},
"node_modules/@vitejs/plugin-react": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz",
"integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.26.10",
"@babel/plugin-transform-react-jsx-self": "^7.25.9",
"@babel/plugin-transform-react-jsx-source": "^7.25.9",
"@types/babel__core": "^7.20.5",
"react-refresh": "^0.17.0"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"peerDependencies": {
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
}
},
"node_modules/@vitejs/plugin-react-swc": {
"version": "3.10.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.10.2.tgz",
"integrity": "sha512-xD3Rdvrt5LgANug7WekBn1KhcvLn1H3jNBfJRL3reeOIua/WnZOEV5qi5qIBq5T8R0jUDmRtxuvk4bPhzGHDWw==",
"license": "MIT",
"dependencies": {
"@rolldown/pluginutils": "1.0.0-beta.11",
"@swc/core": "^1.11.31"
},
"peerDependencies": {
"vite": "^4 || ^5 || ^6 || ^7.0.0-beta.0"
}
},
"node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-types": "^3.0.0",
"negotiator": "^1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/acorn": {
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/body-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
"dev": true,
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^1.0.5",
"debug": "^4.4.0",
"http-errors": "^2.0.0",
"iconv-lite": "^0.6.3",
"on-finished": "^2.4.1",
"qs": "^6.14.0",
"raw-body": "^3.0.0",
"type-is": "^2.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/browserslist": {
"version": "4.24.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
"integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001716",
"electron-to-chromium": "^1.5.149",
"node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.3"
},
"bin": {
"browserslist": "cli.js"
},
"engines": {
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001717",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz",
"integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
},
"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/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"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/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
"license": "Apache-2.0",
"dependencies": {
"clsx": "^2.1.1"
},
"funding": {
"url": "https://polar.sh/cva"
}
},
"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/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"license": "MIT",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"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/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/content-disposition": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.6.0"
}
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true,
"license": "MIT"
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"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/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"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/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"dev": true,
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.151",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.151.tgz",
"integrity": "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==",
"dev": true,
"license": "ISC"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
"integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.4",
"@esbuild/android-arm": "0.25.4",
"@esbuild/android-arm64": "0.25.4",
"@esbuild/android-x64": "0.25.4",
"@esbuild/darwin-arm64": "0.25.4",
"@esbuild/darwin-x64": "0.25.4",
"@esbuild/freebsd-arm64": "0.25.4",
"@esbuild/freebsd-x64": "0.25.4",
"@esbuild/linux-arm": "0.25.4",
"@esbuild/linux-arm64": "0.25.4",
"@esbuild/linux-ia32": "0.25.4",
"@esbuild/linux-loong64": "0.25.4",
"@esbuild/linux-mips64el": "0.25.4",
"@esbuild/linux-ppc64": "0.25.4",
"@esbuild/linux-riscv64": "0.25.4",
"@esbuild/linux-s390x": "0.25.4",
"@esbuild/linux-x64": "0.25.4",
"@esbuild/netbsd-arm64": "0.25.4",
"@esbuild/netbsd-x64": "0.25.4",
"@esbuild/openbsd-arm64": "0.25.4",
"@esbuild/openbsd-x64": "0.25.4",
"@esbuild/sunos-x64": "0.25.4",
"@esbuild/win32-arm64": "0.25.4",
"@esbuild/win32-ia32": "0.25.4",
"@esbuild/win32-x64": "0.25.4"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"dev": true,
"license": "MIT"
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint": {
"version": "9.26.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz",
"integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.20.0",
"@eslint/config-helpers": "^0.2.1",
"@eslint/core": "^0.13.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.26.0",
"@eslint/plugin-kit": "^0.2.8",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
"@modelcontextprotocol/sdk": "^1.8.0",
"@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.6",
"debug": "^4.3.2",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^8.3.0",
"eslint-visitor-keys": "^4.2.0",
"espree": "^10.3.0",
"esquery": "^1.5.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^8.0.0",
"find-up": "^5.0.0",
"glob-parent": "^6.0.2",
"ignore": "^5.2.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"lodash.merge": "^4.6.2",
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
"optionator": "^0.9.3",
"zod": "^3.24.2"
},
"bin": {
"eslint": "bin/eslint.js"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://eslint.org/donate"
},
"peerDependencies": {
"jiti": "*"
},
"peerDependenciesMeta": {
"jiti": {
"optional": true
}
}
},
"node_modules/eslint-plugin-react-hooks": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
"integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
}
},
"node_modules/eslint-plugin-react-refresh": {
"version": "0.4.20",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz",
"integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"eslint": ">=8.40"
}
},
"node_modules/eslint-scope": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
"integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-visitor-keys": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/espree": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.14.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/esquery": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"estraverse": "^5.1.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/esrecurse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
},
"engines": {
"node": ">=4.0"
}
},
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/eventsource": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
"integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eventsource-parser": "^3.0.1"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/eventsource-parser": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz",
"integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/express": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.0",
"content-disposition": "^1.0.0",
"content-type": "^1.0.5",
"cookie": "^0.7.1",
"cookie-signature": "^1.2.1",
"debug": "^4.4.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"finalhandler": "^2.1.0",
"fresh": "^2.0.0",
"http-errors": "^2.0.0",
"merge-descriptors": "^2.0.0",
"mime-types": "^3.0.0",
"on-finished": "^2.4.1",
"once": "^1.4.0",
"parseurl": "^1.3.3",
"proxy-addr": "^2.0.7",
"qs": "^6.14.0",
"range-parser": "^1.2.1",
"router": "^2.2.0",
"send": "^1.1.0",
"serve-static": "^2.2.0",
"statuses": "^2.0.1",
"type-is": "^2.0.1",
"vary": "^1.1.2"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/express-rate-limit": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
"integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": "^4.11 || 5 || ^5.0.0-beta.1"
}
},
"node_modules/extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
"license": "MIT",
"dependencies": {
"is-extendable": "^0.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
"micromatch": "^4.0.8"
},
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/fast-glob/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true,
"license": "MIT"
},
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
}
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"flat-cache": "^4.0.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/finalhandler": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"on-finished": "^2.4.1",
"parseurl": "^1.3.3",
"statuses": "^2.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"license": "MIT",
"dependencies": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/flat-cache": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"flatted": "^3.2.9",
"keyv": "^4.5.4"
},
"engines": {
"node": ">=16"
}
},
"node_modules/flatted": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true,
"license": "ISC"
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"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/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/globals": {
"version": "16.1.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz",
"integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true,
"license": "MIT"
},
"node_modules/gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
"license": "MIT",
"dependencies": {
"js-yaml": "^3.13.1",
"kind-of": "^6.0.2",
"section-matter": "^1.0.0",
"strip-bom-string": "^1.0.0"
},
"engines": {
"node": ">=6.0"
}
},
"node_modules/gray-matter/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/gray-matter/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"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-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-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/i": {
"version": "0.3.7",
"resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz",
"integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==",
"engines": {
"node": ">=0.4"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.19"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
"license": "MIT"
},
"node_modules/is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"dev": true,
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/jiti": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
"node": ">=6"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
}
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/lightningcss": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
"integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==",
"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-darwin-arm64": "1.29.2",
"lightningcss-darwin-x64": "1.29.2",
"lightningcss-freebsd-x64": "1.29.2",
"lightningcss-linux-arm-gnueabihf": "1.29.2",
"lightningcss-linux-arm64-gnu": "1.29.2",
"lightningcss-linux-arm64-musl": "1.29.2",
"lightningcss-linux-x64-gnu": "1.29.2",
"lightningcss-linux-x64-musl": "1.29.2",
"lightningcss-win32-arm64-msvc": "1.29.2",
"lightningcss-win32-x64-msvc": "1.29.2"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz",
"integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==",
"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.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz",
"integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==",
"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.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz",
"integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==",
"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.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz",
"integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==",
"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.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz",
"integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==",
"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.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz",
"integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==",
"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.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz",
"integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==",
"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.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz",
"integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==",
"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.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz",
"integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==",
"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.29.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz",
"integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==",
"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/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-locate": "^5.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dev": true,
"license": "ISC",
"dependencies": {
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.509.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.509.0.tgz",
"integrity": "sha512-xCJHn6Uh5qF6PGml25vveCTrHJZcqS1G1MVzWZK54ZQsOiCVJk4fwY3oyo5EXS2S+aqvTpWYIfJN+PesJ0quxg==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/magic-string": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/marked": {
"version": "15.0.12",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mdast-util-to-hast": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
"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/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/merge-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"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.2",
"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
"integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/minizlib": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
"license": "MIT",
"dependencies": {
"minipass": "^7.1.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/mkdirp": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"license": "MIT",
"bin": {
"mkdirp": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"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/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true,
"license": "MIT"
},
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/node-releases": {
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
"dev": true,
"license": "MIT"
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/oniguruma-parser": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
"integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==",
"license": "MIT"
},
"node_modules/oniguruma-to-es": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.3.tgz",
"integrity": "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==",
"license": "MIT",
"dependencies": {
"oniguruma-parser": "^0.12.1",
"regex": "^6.0.1",
"regex-recursion": "^6.0.2"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
"levn": "^0.4.1",
"prelude-ls": "^1.2.1",
"type-check": "^0.4.0",
"word-wrap": "^1.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"yocto-queue": "^0.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-limit": "^3.0.2"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-to-regexp": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
"integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"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": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pkce-challenge": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
"integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/postcss": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
"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.8",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/property-information": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
"integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
"integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.6.3",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/react": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.26.0"
},
"peerDependencies": {
"react": "^19.1.0"
}
},
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz",
"integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router/node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz",
"integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==",
"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-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
"dev": true,
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
"node_modules/rollup": {
"version": "4.40.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz",
"integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.7"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.40.2",
"@rollup/rollup-android-arm64": "4.40.2",
"@rollup/rollup-darwin-arm64": "4.40.2",
"@rollup/rollup-darwin-x64": "4.40.2",
"@rollup/rollup-freebsd-arm64": "4.40.2",
"@rollup/rollup-freebsd-x64": "4.40.2",
"@rollup/rollup-linux-arm-gnueabihf": "4.40.2",
"@rollup/rollup-linux-arm-musleabihf": "4.40.2",
"@rollup/rollup-linux-arm64-gnu": "4.40.2",
"@rollup/rollup-linux-arm64-musl": "4.40.2",
"@rollup/rollup-linux-loongarch64-gnu": "4.40.2",
"@rollup/rollup-linux-powerpc64le-gnu": "4.40.2",
"@rollup/rollup-linux-riscv64-gnu": "4.40.2",
"@rollup/rollup-linux-riscv64-musl": "4.40.2",
"@rollup/rollup-linux-s390x-gnu": "4.40.2",
"@rollup/rollup-linux-x64-gnu": "4.40.2",
"@rollup/rollup-linux-x64-musl": "4.40.2",
"@rollup/rollup-win32-arm64-msvc": "4.40.2",
"@rollup/rollup-win32-ia32-msvc": "4.40.2",
"@rollup/rollup-win32-x64-msvc": "4.40.2",
"fsevents": "~2.3.2"
}
},
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"depd": "^2.0.0",
"is-promise": "^4.0.0",
"parseurl": "^1.3.3",
"path-to-regexp": "^8.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"queue-microtask": "^1.2.2"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"license": "MIT"
},
"node_modules/scheduler": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
"license": "MIT"
},
"node_modules/section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
"license": "MIT",
"dependencies": {
"extend-shallow": "^2.0.1",
"kind-of": "^6.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/send": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.5",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"fresh": "^2.0.0",
"http-errors": "^2.0.0",
"mime-types": "^3.0.1",
"ms": "^2.1.3",
"on-finished": "^2.4.1",
"range-parser": "^1.2.1",
"statuses": "^2.0.1"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/serve-static": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"parseurl": "^1.3.3",
"send": "^1.2.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"dev": true,
"license": "ISC"
},
"node_modules/sharp": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.2.tgz",
"integrity": "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.4",
"semver": "^7.7.2"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.2",
"@img/sharp-darwin-x64": "0.34.2",
"@img/sharp-libvips-darwin-arm64": "1.1.0",
"@img/sharp-libvips-darwin-x64": "1.1.0",
"@img/sharp-libvips-linux-arm": "1.1.0",
"@img/sharp-libvips-linux-arm64": "1.1.0",
"@img/sharp-libvips-linux-ppc64": "1.1.0",
"@img/sharp-libvips-linux-s390x": "1.1.0",
"@img/sharp-libvips-linux-x64": "1.1.0",
"@img/sharp-libvips-linuxmusl-arm64": "1.1.0",
"@img/sharp-libvips-linuxmusl-x64": "1.1.0",
"@img/sharp-linux-arm": "0.34.2",
"@img/sharp-linux-arm64": "0.34.2",
"@img/sharp-linux-s390x": "0.34.2",
"@img/sharp-linux-x64": "0.34.2",
"@img/sharp-linuxmusl-arm64": "0.34.2",
"@img/sharp-linuxmusl-x64": "0.34.2",
"@img/sharp-wasm32": "0.34.2",
"@img/sharp-win32-arm64": "0.34.2",
"@img/sharp-win32-ia32": "0.34.2",
"@img/sharp-win32-x64": "0.34.2"
}
},
"node_modules/sharp/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/shiki": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-3.6.0.tgz",
"integrity": "sha512-tKn/Y0MGBTffQoklaATXmTqDU02zx8NYBGQ+F6gy87/YjKbizcLd+Cybh/0ZtOBX9r1NEnAy/GTRDKtOsc1L9w==",
"license": "MIT",
"dependencies": {
"@shikijs/core": "3.6.0",
"@shikijs/engine-javascript": "3.6.0",
"@shikijs/engine-oniguruma": "3.6.0",
"@shikijs/langs": "3.6.0",
"@shikijs/themes": "3.6.0",
"@shikijs/types": "3.6.0",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
}
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"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/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"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/strip-bom-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/tailwind-merge": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz",
"integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/tailwindcss": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz",
"integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==",
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/tar": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
"license": "ISC",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.0.1",
"mkdirp": "^3.0.1",
"yallist": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tar/node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/tinyglobby": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"license": "MIT",
"dependencies": {
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinyglobby/node_modules/fdir": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"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/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"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/tw-animate-css": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.9.tgz",
"integrity": "sha512-9O4k1at9pMQff9EAcCEuy1UNO43JmaPQvq+0lwza9Y0BQ6LB38NiMj+qHqjoQf40355MX+gs6wtlR6H9WsSXFg==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Wombosvideo"
}
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
"dev": true,
"license": "MIT",
"dependencies": {
"content-type": "^1.0.5",
"media-typer": "^1.1.0",
"mime-types": "^3.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/typescript-eslint": {
"version": "8.32.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.0.tgz",
"integrity": "sha512-UMq2kxdXCzinFFPsXc9o2ozIpYCCOiEC46MG3yEh5Vipq6BO27otTtEBZA1fQ66DulEUgE97ucQ/3YY66CPg0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.32.0",
"@typescript-eslint/parser": "8.32.0",
"@typescript-eslint/utils": "8.32.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/unist-util-is": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
"integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==",
"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.0.0",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
"integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0",
"unist-util-visit-parents": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-visit-parents": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz",
"integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
},
"bin": {
"update-browserslist-db": "cli.js"
},
"peerDependencies": {
"browserslist": ">= 4.21.0"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"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.2",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz",
"integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==",
"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": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.34.9",
"tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"jiti": ">=1.21.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"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/vite/node_modules/fdir": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/vite/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true,
"license": "ISC"
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.24.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz",
"integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-to-json-schema": {
"version": "3.24.5",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz",
"integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==",
"dev": true,
"license": "ISC",
"peerDependencies": {
"zod": "^3.24.1"
}
},
"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"
}
}
}
}

49
package.json Normal file
View file

@ -0,0 +1,49 @@
{
"name": "systems-obscure",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build:posts": "node scripts/generate-post-index.js",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-navigation-menu": "^1.2.12",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-toggle": "^1.1.8",
"@shikijs/colorized-brackets": "^3.6.0",
"@tailwindcss/vite": "^4.1.6",
"@vitejs/plugin-react-swc": "^3.10.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"gray-matter": "^4.0.3",
"i": "^0.3.7",
"lucide-react": "^0.509.0",
"marked": "^15.0.12",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.6.0",
"sharp": "^0.34.2",
"shiki": "^3.6.0",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.6"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@types/node": "^22.15.17",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"tw-animate-css": "^1.2.9",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"
}
}

11
posts/a-human-being-is.md Normal file
View file

@ -0,0 +1,11 @@
---
title: "A human being is..."
slug: /a-human-being-is/
date: 2024-08-18
tags: ["quotes"]
---
> A human being is a packet of propagating information that dissipates at death.
I can't remember who said this or where it comes from but I just found it in an
old notebook.

View file

@ -0,0 +1,194 @@
---
title: "Activity Log backend"
slug: /activity-log-backend/
date: 2024-07-19
tags: ["log", "aws", "typescript", "python", "bash"]
---
I have just added a new feature to the site:
[Activity Log](https://systemsobscure.blog/activity-log/), which presents my
personal time tracking entries. This is how I configured the backend.
![](./img/activity-log-diagram-darker.png)
On my local machine I use [TimeWarrior](https://timewarrior.net/docs/what/) to
record the time I spend on different activities. It runs from the terminal and
can be exported to JSON.
To access the data remotely, I needed to store it in a database and be able to
regularly export the time entries from my machine to the remote DB.
I created a simple AWS Lambda with a few endpoints:
- A POST that receives data and adds it to the database.
- A GET at `/count` that returns a count of the entries for each day for the
last twelve months
- A GET at `/month` that returns the entries for the last month
- A GET at `/day` that returns the entries for the specified single date
The idea is that the frontend will first present all the entries for the current
month in a table. Along with this there will be histogram similar to GitHub's
commit graph that will present a colour coded representation of the amount of
activity for each day in the last year. When the user clicks on a day, a request
is made for the entries for that day. That way I don't have to load all the
entries into memory at once.
In development, I used MySQL but it turns out that production SQL databases are
prohibitively expensive on AWS. This figures as MySQL requires a permanently
running server, contravening the serverless architecture that makes AWS
otherwise affordable on the Free Tier.
After some research I decided to use DynamoDB. This didn't immediately occur to
me because, in my ignorance, I assumed a key-value database only permits one key
to one value and my table contains multiple fields for each row. In fact, after
specifying a unique _primary key_, you can associate it with any number of
values (known as "attributes"), thus creating a "table-like" data structure.
The nomenclature for DynamoDB is quite confusing but I was able to create a
simple table-like structure with the following schema:
```json
{
"TableName": "TimeEntries",
"AttributeDefinitions": [
{ "AttributeName": "activity_start_end", "AttributeType": "S" },
{ "AttributeName": "year", "AttributeType": "S" }
],
"KeySchema": [{ "AttributeName": "activity_start_end", "KeyType": "HASH" }],
"GlobalSecondaryIndexes": [
{
"IndexName": "YearIndex",
"KeySchema": [
{ "AttributeName": "year", "KeyType": "HASH" },
{ "AttributeName": "start", "KeyType": "RANGE" }
]
}
],
"ProvisionedThroughput": {
"ReadCapacityUnits": 1,
"WriteCapacityUnits": 1
}
}
```
The attribute `activity_start_end` is my primary key which is basically
equivalent to the primary key in SQL: the value I use to uniquely individuate
each entry in the table. It is a simple concatenation of the fields
`activity_type`, `start`, `end`. The latter two are ISO timestamps, ensuring
uniqueness.
A _global secondary index_ (GSI) is an attribute in addition to the primary key
that you may use to group entries. By suppling a GSI with your query you can
reduce the compute required for lookups. By using `year` as a GSI I ensure that
the query will only run against values matching the specified year.
When working locally, I used Docker to create a DynamoDB image. As the AWS SAM
software (used for running the Lambda and API Gateway locally) also uses a
Docker container (that you can't inspect or modify) it was necessary to specify
a bridging network in the `docker-compose.yml` so that each container could
communicate:
```yml
services:
dev:
image: amazon/dynamodb-local
container_name: timetracking_dynamodb_dev
ports:
- "8000:8000"
volumes:
- "/home/thomas/repos/lambdas/node-js/time-tracking/data/dev:/home/dynamodblocal/data/dev"
command:
"-jar DynamoDBLocal.jar -dbPath /home/dynamodblocal/data/dev --sharedDb"
networks:
- sam-local
networks:
sam-local:
driver: bridge
```
With the server running I could then view the database using Amazon's _NoSQL
Workbench_ client:
![NoSQL workbench](./img/no-sql-workbench.png)
Creating the Lambda with TypeScript was simple enough using the AWS SDK. Here is
the function that I use to return the entries for the month and specific dates:
```ts
import { DynamoDBClient } from "@aws-sdk/client-dynamodb"
import { DynamoDBDocumentClient, QueryCommand } from "@aws-sdk/lib-dynamodb"
import { generateDates, TPeriod } from "./generateDates"
interface ITimeEntry {
activity_start_end: string
year: string
start: string
end: string
activity_type: string
duration: number
description: string
}
const getTimeEntries = async (
client: DynamoDBClient,
timePeriod: TPeriod
): Promise<ITimeEntry[]> => {
const documentClient = DynamoDBDocumentClient.from(client)
const dateParams = generateDates()[timePeriod]
const params = {
TableName: "TimeEntries",
IndexName: "YearIndex",
KeyConditionExpression:
"#yr = :year AND #start BETWEEN :start_date AND :end_date",
ExpressionAttributeNames: {
"#yr": "year",
"#start": "start",
},
ExpressionAttributeValues: {
":year": dateParams.year,
":start_date": dateParams.start,
":end_date": dateParams.end,
},
}
const command = new QueryCommand(params)
const response = await documentClient.send(command)
return (response?.Items as ITimeEntry[]) || []
}
export { getTimeEntries, ITimeEntry }
```
(The `DynamoDBDocumentClient` proved essential. It ensures that each row comes
back as an array of objects matching the shape of `ITimeEntry`. Without this,
the data returns in a nested format specific to DynamoDB that is cumbersome to
work with.)
Outside of the Lambda and setting up the database, it was necessary to write
some scripts to glue the different parts together:
- a Python
[script](https://github.com/thomasabishop/lambdas/blob/main/node-js/time-tracking/scripts/export_timewarrior_entries.py)
that exports the entries from TimeWarrior, parsing them into the format the
database expects
- a Python
[script](https://github.com/thomasabishop/lambdas/blob/main/node-js/time-tracking/scripts/upload_daily_entries.py)
(running on a cron timer) that runs the preceding export script every hour and
uploads to the remote DB using the Lambda's POST endpoint, sending a
notification to my
[Slack channel](https://systemsobscure.blog/slack-notification-center/)
- a Bash
[ script](https://github.com/thomasabishop/lambdas/blob/main/node-js/time-tracking/scripts/migrate.sh)
to migrate the contents of the production database to my local Docker instance
(useful when working on the frontend locally)
- a Bash
[script](https://github.com/thomasabishop/lambdas/blob/main/node-js/time-tracking/scripts/seed.sh)
to seed the production and local databases from a CSV
That's basically it. The biggest pain point was getting my DynamoDB Docker
instance to communicate with the default SAM instance. That, and converting my
SQL version into DynamoDB. I wouldn't say I find DynamoDB particularly
straightforward or fun to use but I'll put up with it because it costs like
£00.02 per month compared to the £54.00 they were asking for MySQL.

View file

@ -0,0 +1,42 @@
---
title: "Alien Blood VS Code theme"
slug: /alien-blood-vscode-theme/
date: 2022-01-01
tags: ["project", "productivity"]
---
I'm endlessly toggling through colour themes in VS Code, combining them with
different fonts and icons to approximate a Platonic ideal of the optimal working
environment. I am never satisfied. There is always some aspect that displeases
me.
![The Alien Blood terminal colours](./img/alien_blood_terminal_colours.png)
Over the last month or so I decided to resolve the issue once and for all by
creating my own custom theme.
I have always really enjoyed the Alien Blood colours that are one of the default
themes of the iTerm2 terminal emulator for Mac. I decided I would use this as my
base and try to construct a full syntax and UI scheme from the terminal
pallette. This wasn't easy because the colour palettes of terminal themes are
more austere than those designed for syntax-highlighting. It took lots of
tweaking to try and make the resulting theme visually harmonious but not so
homogenous that you cannot distinguish the syntax at glance.
![Alien Blood VS Code theme showing TypeScript highlighting](./img/alien_blood_syntax_demo.png)
I was able to achieve this without introducing any additional colours to the
default Alien Blood palette. The theme can be demoed in the browser
[here](https://vscode.dev/theme/ThomasBishop.alien-blood/Alien%20Blood%20) and
downloaded from the
[extension marketplace](https://marketplace.visualstudio.com/items?itemName=ThomasBishop.alien-blood).
You can also view the theme on
[GitHub](https://github.com/thomasabishop/alien-blood-vscode).
I'm particularly happy with the rendering of TypeScript and Angular components
which is what I am editing the majority of the time at work, similarly with HTML
and CSS. However I think there is room for improvement when it comes to plain
JavaScript and other filetypes such as shell scripts. In these cases the
highlighting is too homogenous and dark but now I have the basic palette
completed I can add additional rules for specific programming languages in
future releases.

View file

@ -0,0 +1,195 @@
---
title: "How I backup my primary machine"
slug: /backing-up-primary-machine/
date: 2023-01-08
tags: ["log", "linux"]
---
I use the `rsnapshots` package to run sequenced backups of my ThinkPad T15
running Arch Linux.
`rsnapshots` is based on the `rsync` utility and makes it easy to maintain
backups over several timeframes. I run hourly, daily, weekly, and monthly
backups, sequenced using `systemd` timers.
## Preparing the external disk
I store my backups on an external 500GB SSD.
First I partition the disk:
```
fdisk /dev/sda
d # delete the existing partitions
n # start a new partition
+500GB # specify size
w # write the partition to the disk
```
I now have a 500GB partition at `/dev/sda/sda1`
Next I create the file system:
```
mkfs -t ext4 /dev/sda1
```
And I label my disk so that I it has a readable name rather than the default
GUID that Linux will apply.
```
e2label /dev/sda1 archbish_backups
```
At this point you would create or specify a mount directory and mount the
partition with `mount`, also adding it to your `fstab` file to ensure that the
disk mounts to the same location in future.
I haven't chosen to do this because I use the KDE Plasma desktop environment and
it automatically mounts any connected drives to `/run/media`. However in order
to check the partition label and ensure the above processes have been successful
I will disconnect and reconnect the device.
Now when I run `lsblk` to list the block devices on the machine, I see my new
disk and partition:
```
lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sda 8:0 0 465.8G 0 disk
└─sda1 8:1 0 465.8G 0 part /run/media/thomas/archbish_backups
nvme0n1 259:0 0 476.9G 0 disk
├─nvme0n1p1 259:1 0 512M 0 part
├─nvme0n1p2 259:2 0 11.2G 0 part [SWAP]
└─nvme0n1p3 259:3 0 465.3G 0 part /
```
Typically you won't have access to the partition or mount directory yet. You can
run the following against your username to ensure access:
```
sudo chown -R <username>:users /run/media/<username>/
```
## Configure snapshot backups with rsnapshot
I install `rsnapshot` and `rsync` as a dependency:
```
pacman -Sy rsync rsnapshot
```
`rsnapshot` is setup entirely through its config file: `/etc/rsnapshot.conf`.
When you install the package the default config file contains lots of
instructions and it's mostly a case of removing the comments for the
functionality you require. The
[Arch wiki](https://wiki.archlinux.org/title/rsnapshot) provides an exhaustive
account but the key parts are as follows:
```
# Set the snapshot root directory (the external HDD you plan to use)
snapshot_root /run/media/thomas/archbish_backups
# Set the backup intervals
retain hourly 24
retain daily 7
retain weekly 4
retain monthly 12
# Name the dir you want to snapshot and what it should be called on the external disk
backup /home/ localhost
```
So, obviously I am taking hourly, daily, weekly and monthly snapshots. In total,
this gives me a years worth of retention whilst minimising the space taken by
the older backups. (If I wanted to keep a record spanning several years, I could
just make a copy the latest monthly backup in a year's time.)
The numbers next to the names indicate the retention period. After 24 hours, the
oldest hourly backup becomes the daily backup; after seven days the oldest daily
backup becomes the weekly backup; after four weeks the oldest weekly backup
becomes the monthly backup and so on.
Now we need to automate the execution of `rsnapshot` at the times designated in
the configuration file. We'll do this with a `systemd` service file and several
timers. Each file created will be located at `/etc/systemd/system/`
First we create the service file. This will be a oneshot service that is run by
the timers:
```
[Unit]
Description=rsnapshot (%I) backup
[Service]
Type=oneshot
Nice=19
IOSchedulingClass=idle
ExecStart=/usr/bin/rsnapshot %I
```
Then we have to create a timer file for each of the intervals: hourly, daily,
weekly, and monthly. Here's the hourly and monthly files to give an idea:
```
# /etc/systemd/system/rsnapshot-hourly.timer
[Unit]
Description=rsnapshot hourly backup
[Timer]
# Run every hour at 15mins past the hour
OnCalendar=*:15
Persistent=true
Unit=rsnapshot@hourly.service
[Install]
WantedBy=timers.target
```
```
# /etc/systemd/system/rsnapshot-monthly.timer
[Unit]Description=rsnapshot monthly backup
[Timer]
# Run once per month at 3:30 local time
OnCalendar=*-*-1 03:30:00
Persistent=true
Unit=rsnapshot@monthly.service
[Install]
WantedBy=timers.target
```
Let's check one of our timers:
```
systemd-analyze calendar "*:15"
Original form: *:15
Normalized form: *-*-* *:15:00
Next elapse: Sun 2023-01-08 15:15:00 GMT
(in UTC): Sun 2023-01-08 15:15:00 UTC
From now: 27min left
```
Nice. Now we need to enable and start the timers. For each timer run:
```
systemctl enable rsnapshot-[interval].timer
systemctl start rsnapshot-[interval].timer
```
Oh it's 15:21, let's check the first hourly snapshot was taken:
```
journalctl -u rsnapshot@hourly
Jan 08 15:15:04 archbish systemd[1]: Starting rsnapshot (hourly) backup...
```
Great. After a few hours we can see the different snapshots mounting up in the
backup directory:
![](img/rnapshot-dolphin-file-viewr.png)

13
posts/beckett-quote.md Normal file
View file

@ -0,0 +1,13 @@
---
title: "Beckett quote"
slug: /beckett-quote/
date: 2023-10-19
tags: ["quotes", "reading"]
---
> For what is this shadow of the going in which we come, this shadow of the
> coming in which we go, this shadow of the coming and the going in which we
> wait, if not the shadow of purpose, of the purpose that budding withers, that
> withering buds, whose blooming is a budding withering.
Samuel Beckett, _Watt_ (1953)

View file

@ -0,0 +1,96 @@
---
title: "Visit to Bletchley Park and The National Museum of Computing"
slug: /bletchley-park-tnmoc-holiday/
date: 2024-08-07
tags: ["personal", "beige", "gruvbox"]
---
As part of our holiday this year my girlfriend and I went to visit
[Bletchley Park](https://bletchleypark.org.uk/) and
[The National Museum of Computing](https://www.tnmoc.org/).
With our customary taste for luxury, we stayed in one of the three Premier Inns
in Milton Keynes. Milton Keynes is strange: a car park in search of a town.
However, we had the good fortune to overlook one of its supermalls. This is
notable only because it _looks exactly like a sandworm in Dune_:
![The Milton Keynes sandworm](./img/mk-worm.jpg)
Bletchley was superb. A model of how to run a heritage project and museum. We
spent about five hours on site stopping for lunch and an excellent cup of tea.
As you work your way through the huts you pass through each region of what was a
global signals intelligence factory: collection, decryption, evaluation, and
finally dissemination.
The huts concerned with decryption were naturally the most compelling. There was
life size replica of the Bombe computer used to derive the daily settings of the
Enigma machine. It had spinning and clicking rotars however this was a
simulation rather than a working reconstruction.
![Replica of the wartime Bombe Computer at Bletchley](../posts/img/bombe-at-bletchley.jpg)
![Author viewing Turing's notes on the Bombe computer](../posts/img/turing-notes-bombe.jpg)
Probably the biggest highlight for me was standing at "the birthplace of the
modern computer": the hut where the Colossus computer was used to decipher the
Lorenz messages of the German high command.
![Standing outside the hut where the Colosus code breaking computer was
housed](../posts/img/colossus-birthplace-bletcley.jpg)
Although not a modern computer in the sense of being general-purpose (it could
only be used for breaking this type of cipher and and was not programmable), it
was the first to use vacuum-tubes for logic operations, rather than
electro-mechanical switches and relays. This made it fully electronic and
therefore much quicker and with greater combinatorial range.
![Reconstruction of the Colossus computer](../posts/img/colossos-reconstruction.jpg)
This was the insight of
[Tommy Flowers](https://en.wikipedia.org/wiki/Tommy_Flowers) (a working class
hero if ever there was), who designed and built it. He proposed using
vacuum-tubes from his experience with telephony at the Post Office Research
Station. This was met with scepticism and at one point he resorted to using his
own money (never properly remunerated) to build it. He was vindicated. Not only
did the machine prove critical in the final stages of the War (confirming the
Nazis had bought the D-Day deception), it proved the speed and viability of
purely-electronic components that would ultimately lead to the transistor and
integrated circuit in later decades.
![Sculpture of Alan Turing at Bletchley Park](./img/turing_sculpture.jpg)
The next day we went to the National Museum of Computing which is unaffiliated
with Bletchley but located on the same site.
![Outside The National Museum of Computing](./img/outside-tmoc.jpg)
This was a different experience. Certainly less polished and perhaps a bit
forbidding for those not already well versed in computer lore.
This said, it had it's own scruffy charm and is clearly a labour of love. During
our visit there were OG volunteer computer engineers actively working on the
reconstructions.
It was a complete cornicopia of retro computers and we had a high time
marvelling at the sheer amount of beige and retro-futurist design.
Here are some of my highlights...
![The Elliott 900](./img/elliott-900.jpg)
![An array of vacuum-tubes (thermionic valves)](./img/thermionic-valves.jpg)
![Looking at circuit boards](./img/circuit-boards.jpg)
![The first IBM Personal Computer](./img/ibm-pc.jpg)
![Nice keycaps](./img/keycaps.jpg)
![Gaby with BBC Micro](./img/gaby-bbc-micro.jpg)
![Gruvbox keycaps!](./img/gruvbox_calculator.jpg)
![Gaby with an array of PCs](./img/gaby-with-pcs-again.jpg)
![PDP-11 programming instructions](./img/pdp-decompressed.jpg)

View file

@ -0,0 +1,110 @@
---
title: "Converting a Pixel 2 XL into a private DAP"
slug: /converting-pixel-phone-to-dap/
date: 2024-12-15
tags: ["projects"]
---
I recently read
[_The Age of Surveillance Capitalism_](https://en.wikipedia.org/wiki/The_Age_of_Surveillance_Capitalism)
. This book and initiatives like the
[Opt Out Project](https://www.optoutproject.net) have motivated me to try and
improve my digital privacy.
Some will see this as a fool's errand and I agree to some extent. However it
isn't something I expect to complete overnight. I view the journey as just as
important as the destination as it provides opportunities to acquire new skills,
discover new technologies, and work on projects that have utility in my daily
life.
My first objective was to do away with my Spotify subscription and consume music
from a dedicted offline device that plays albums I actually own.
I researched modern-day MP3 players but this is a pretty dead market these days
and all the devices were ugly or surplus to my requirements. There is a
community of hard-core audiophiles who have rechristened MP3 players _digital
audio players_ (DAPs) but most of the recommended devices are very expensive and
my goal is to try and keep the overall anti-surveillance project affordable,
recycling and building my own solutions where I can.
It occurred to me that I could just repurpose an old phone into a single-purpose
computer for playing music. Appropriately enough, I have an old Google Pixel 2
XL from 2017 - the surveillance capitalist device _par excellence_.
It is able to play FLAC files natively and its 128GB harddrive should be more
than sufficient for my library.
My first task was to "de-Google" the phone by finding a version of Android that
respects privacy and doesn't bundle surveillance-ware. This was trickier that I
expected. Not because there isn't such software but because phones have a more
specific hardware set with greater variation than laptops of desktop computers.
You have to find an OS that is compatible with your hardware and which is still
regularly maintained even though the hardware is, in my case, eight years old
and long since superseded by more recent devices.
Luckily [LineageOS](https://lineageos.org) offers a build that works with the
Pixel 2 XL and which is fairly lightweight, allowing me to preserve greater disk
space for the audio files.
Having enabled "Developer Mode" on the Pixel, I needed to install a few CLI
tools on my Arch Linux machine that would enable me to interface with the
device.
I installed `android-tools` and `android-udev`. `android-tools` includes `adb`
which allows me to communicate with an Android device over USB, transfer files,
and access the shell from another Linux device. It also includes `fastboot`
which unlocks the Pixel bootloader, necessary to install third-party recovery
software through which I can flash a new OS ROM to the device. I used
`android-udev` to grant myself access to the Pixel from my Linux PC.
![Bootloader unlocked](./img/lineage-bootloader.jpeg)
Having gained access to the device remotely I used `adb` to unlock the
bootloader and then transferred over the [TWRP](https://twrp.me/about/) recovery
software and the latest build of LineageOS, tagged to Android v.12.
Then, on the device, I booted into TWRP. From here I wiped the data and
installed LineageOS. This took a few attempts to work. It kept booting into the
recovery menu for some reason but eventually it just worked.
![Running the recovery software](./img/lineage-recovery.jpeg)
Once the new OS was installed there wasn't much else to do. I deleted the few
apps included with LineageOS that I didn't need and installed F-Droid. F-Droid
is a de-Googled version of Google's Play Store that serves as a package manager
for FOSS Android apps.
![LineageOS installed](./img/lineage.jpeg)
Using F-Droid, I installed a few music apps to experiment with but ended up
finding the default LineageOS player satisfactory. I also installed Duck Duck Go
as my browser and KDE Connect. Although I intend to mostly keep the device
offline, it's handy to have a browser to source images for the music player.
KDE Connect allows me to connect to the Pixel from my PC over WiFi. This is
necessary for transferring the audio files. It also allows me to control media
on the PC from the phone although I doubt I will have much use for this.
I am still in the process of recreating my Spotify library with albums I own. I
have a lot of albums in MP3 on an old external HD from the pre-streaming era. I
have also been able to use the Internet Archive as well as buying albums direct
from artists on Bandcamp. For very obscure stuff that was originally released on
tape and circulated online I am usually able to find torrents, although some
stuff is very hard to track down. I'm planning on getting a CD drive so that I
can rip CDs I find in second-hand shops and fairs. If I go full
"digital-hoarder", I may even get a vinyl-to-digital converter turnable
eventually, then I can exploit my friends' LP collections.
![Album list within the DAP](./img/lineage-library.jpeg)
One annoying thing is that the metadata for the music files will often be
missing or incomplete. For instance the tracks on an album might be out of order
or lacking the album cover. I use [kid3](https://kid3.kde.org) to view and edit
the metadata so that the tracks are recognised properly by the player with
release year and album art.
![kid3 for managing album metadata](./img/kid3-screenshot.png)
My next challenge will be tackling audiobooks and podcasts. Ideally I would like
to download them as files to the DAP and avoid streaming services. More on this
to follow.

View file

@ -0,0 +1,21 @@
---
title: "DeLillo, alignment"
slug: /delillo-alignment/
date: 2023-04-16
tags: ["quotes", "reading"]
---
I am reading Don DeLillo's _Underworld_ (1997) at the moment. As we worry about
the [problem of alignment](https://en.wikipedia.org/wiki/AI_alignment) in the
wake of GPT-4 the following passage seemed especially apposite.
> I was driving a Lexus through a rustling wind. This is a car assembled in a
> work area that's completely free of human presence. Not a spot of mortal sweat
> except, okay, for the guys who drive the product out of the plant &mdash;
> allow a little moisture where they grip the wheel. The system flows forever
> onward, automated to priestly nuance, every gliding movement back-referenced
> for prime performance. Hollow bodies coming in endless sequence. There's
> nobody on the line with caffeine nerves or a history of clinical depression.
> Just the eerie weave of chromium alloys carried in interlocking arcs, block
> iron and asphalt sheeting, soaring ornaments of coachwork fitted and merged.
> Robots tightening bolts, programmed drudges that do not dream of family dead.

View file

@ -0,0 +1,23 @@
---
title: "Gruvbox image filters"
slug: /gruvbox-image-filters/
date: 2024-11-16
tags: ["gruvbox", "beige", "log"]
---
I've been experimenting with an
<a href="https://en.wikipedia.org/wiki/ImageMagick">ImageMagick</a> script to
create filters for images that I use as my screensaver. I applied a
Gruvbox-themed colour swap and manipulated the dithering to create a retro
effect with highlights. I was quite surprised how easy and effective this proved
to be. Below are some of the ones I like best.
![The composer Éliane Radigue manipulating an ARP synthesiser.](./img/radigue_gruvbox.png)
![Operators in front of the MANIAC computer.](./img/Operators_in_front_of_the_MANIAC_gruvbox.png)
![Power station control panel image I found on Unsplash.](./img/control-panel_gruvbox.png)
![Composer Laurie Speigel working with a computer at Bell Labs.](./img/laurie-burn_gruvbox.png)
![The Twin Towers](./img/tt-two_gruvbox.png)

View file

@ -0,0 +1,133 @@
---
title: "How I deploy this site"
slug: /how-I-deploy-this-site/
date: 2023-01-02
tags: ["log", "linux"]
---
I want to make a note of how this blog is maintained and deployed for future
reference as the AWS process is quite involved.
I use AWS because I have to know at least one cloud/serverless provider well and
we use AWS at work. I intend to take the AWS Certified Developer exam eventually
so knowing how to deploy a frontend application is useful.
## Frontend: Gatsby.js
Nothing special here. I am using the React-based [Gatsby]() framework to create
the frontend and as you can see, there isn't much to the site. Just a homepage
and a bunch of posts. I used the Gatsby starter template, changed the fonts and
styled it in the manner of my [AlienBlood]() theme.
## Deployment: AWS S3, CloudFront, Route 53, Certificate Manager
- First I uploaded the build directory to a bucket on S3 making sure to set the
permissions to public and to specify that the bucket hosts a static website.
- Then I created a hosted zone using AWS Route 53. This is necessary for the
bucket to be publicly accessible as a domain on the internet. I purchased the
domain name from GoDaddy and uploaded the AWS nameservers there. Cue 24 hours
propagation time.
- All sites should be served over `https` so I requested a public SSL
certificate from AWS Certificate Manager.
- You can't serve an S3 bucket as `https` by default. The solution is to use
CloudFront as a CDN and then specify a redirection rule from `http` to
`https`. So you create the CloudFont instance and generate an endpoint. This
endpoint is then added to the Route 53 specifications as an A record. That
propagates in a matter of seconds and now the site is securely hosted.
## Middlewear: AWS Lambda
An annoying thing about CloudFront is that it won't recognise pages other than
the root `index.html` on refresh. So if I went to this page
(https://systemsobscure.blog/how-I-deploy-this-site/), on refresh it becomes a
403 because the file format is not `how-I-deploy_this_site/index.html`.
To get around this the custom is to use a Lambda function that intercepts
requests and add the trailing `index.html`:
```js
exports.handler = (event, context, callback) => {
// Extract the request from the CloudFront event that is sent to Lambda@Edge
var request = event.Records[0].cf.request
// Extract the URI from the request
var olduri = request.uri
// Match any '/' that occurs at the end of a URI. Replace it with a default index
var newuri = olduri.replace(/\/$/, "/index.html")
// Replace the received URI with the URI that includes the index page
request.uri = newuri
// Return to CloudFront
return callback(null, request)
}
```
You then set this function to trigger on page requests to the CloudFront
distribution that is handling the routing and the problem is resolved.
## Continuous deployment: GitHub Actions
It's slightly onerous to have to manually trigger a deploy to S3 every time I
write a new post. A better scenario would be to trigger a build and a deployment
to S3 every time I push to the `main` branch on GitHub. I could do this from
within AWS but I've chosen to have GitHub communicate with AWS rather than the
other way around. That way I get some experience of using Actions.
My GitHub Action declaration runs the standard `gatsby build` command on push
and also runs the following NPM script once the build completes:
```bash
gatsby-plugin-s3 deploy --yes; aws cloudfront create-invalidation --distribution-id <REDACTED> --paths '/*';",
```
This uses a Gatsby plugin to deploy to S3 and clear the Cloudfront cache.
In order for this command to run from GitHub I had to create a "GitHub" user and
custom permissions file in AWS IAM. This gives me an Access Key ID and secret
which the GitHub Action can use to authenticate the deployment. I save these as
secrets within the repository settings in GitHub and now the whole Action
declaration works.
Here is the GitHub Action in full:
```yaml
name: Deploy systemsobscure.blog
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 18
- name: Caching Gatsby
id: gatsby-cache-build
uses: actions/cache@v2
with:
path: |
public
.cache
node_modules
key: ${{ runner.os }}-systemsobscure-site-build-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-systemsobscure-site-build-
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Set AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
- name: Deploy to S3
run: npm run deploy
```

View file

@ -0,0 +1,32 @@
---
title: "Bash script: delete unused images"
slug: /image-deletion-bash-script/
date: 2023-04-16
tags: ["log", "bash"]
---
When I'm working on my technical notes I often insert images, diagrams,
screenshots etc. Often, I end up creating multiple images that I end up not
using. I don't like commiting dead bytes to the repo so I wrote a short script
that reads each file in the image directory and looks to see if it is referenced
in any Markdown files. If it isn't, the image is deleted.
```bash
find /home/thomas/repos/eolas/_img -type f | while read filename; do
rg "${filename##*/}" ../ --type markdown >/dev/null 2>&1
if [ "$?" -eq 1 ]; then
echo "Deleted unused image: ${filename##*/}"
rm $filename
fi
done
```
Example:
```sh
./clean_image_directory.sh
Deleted unused image: multiplication_03.gif
Deleted unused image: Pasted_image_20220319174839.png
Deleted unused image: multiplication_04.gif
Deleted unused image: multiplication_02.gif
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.0"
width="225"
height="225"
id="svg2"
sodipodi:version="0.32"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="BJT_NPN_symbol.svg"
inkscape:export-filename="bjt-again.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata19">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
inkscape:window-height="1138"
inkscape:window-width="1878"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
guidetolerance="10.0"
gridtolerance="10.0"
objecttolerance="10.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base"
width="150px"
height="150px"
inkscape:zoom="1.4142136"
inkscape:cx="43.487066"
inkscape:cy="60.457628"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:current-layer="svg2"
showgrid="true"
inkscape:grid-bbox="false"
inkscape:grid-points="true"
inkscape:guide-bbox="false"
inkscape:guide-points="true"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:window-maximized="0" />
<defs
id="defs4" />
<g
id="g311"
transform="translate(35.99999,37.5)">
<path
id="path1307"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:3px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 89,99 79,96 86,89 Z"
sodipodi:nodetypes="cccc" />
<path
id="path1309"
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:3px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 70,80 20,20 v 35"
sodipodi:nodetypes="ccc" />
<path
id="path1321"
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:3px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 70,70 90,50 V 15"
sodipodi:nodetypes="ccc" />
<path
id="path1325"
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:3px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 70,75 H 15"
sodipodi:nodetypes="cc" />
<path
id="path2202"
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:2.44444;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 112.7778,75.000015 c 0,17.550387 -14.227398,31.777785 -31.777785,31.777785 -17.550387,0 -31.777785,-14.227398 -31.777785,-31.777785 0,-17.550387 14.227398,-31.777785 31.777785,-31.777785 17.550387,0 31.777785,14.227398 31.777785,31.777785 z" />
<path
id="path3078"
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:3px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 70,55 V 95"
sodipodi:nodetypes="cc" />
<text
x="94.242188"
y="34.086914"
style="font-style:normal;font-weight:normal;font-size:18px;line-height:0%;font-family:'Bitstream Vera Sans';fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text3963"
xml:space="preserve"><tspan
x="94.242188"
y="34.086914"
id="tspan3965">C</tspan></text>
<text
x="94.822266"
y="128.7627"
style="font-style:normal;font-weight:normal;font-size:18px;line-height:0%;font-family:'Bitstream Vera Sans';fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text3967"
xml:space="preserve"><tspan
x="94.822266"
y="128.7627"
id="tspan3969">E</tspan></text>
<text
x="20"
y="70"
style="font-style:normal;font-weight:normal;font-size:18px;line-height:0%;font-family:'Bitstream Vera Sans';fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text3971"
xml:space="preserve"><tspan
x="20"
y="70"
id="tspan3973">B</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.0"
width="850"
height="1000"
id="svg2"
sodipodi:version="0.32"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="BJT_transistor_symbol.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata24">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
inkscape:window-height="884"
inkscape:window-width="1683"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
guidetolerance="10.0"
gridtolerance="10.0"
objecttolerance="10.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base"
inkscape:zoom="1.0578317"
inkscape:cx="173.94071"
inkscape:cy="221.67987"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:current-layer="svg2"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:window-maximized="0" />
<defs
id="defs4">
<marker
refX="0"
refY="0"
orient="auto"
style="overflow:visible"
id="TriangleOutL">
<path
d="M 5.77,0 L -2.88,5 L -2.88,-5 L 5.77,0 z "
transform="scale(0.8,0.8)"
style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;marker-start:none"
id="path4913" />
</marker>
<marker
refX="0"
refY="0"
orient="auto"
style="overflow:visible"
id="Arrow1Mend">
<path
d="M 0,0 L 5,-5 L -12.5,0 L 5,5 L 0,0 z "
transform="scale(-0.4,-0.4)"
style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;marker-start:none"
id="path5005" />
</marker>
<marker
refX="0"
refY="0"
orient="auto"
style="overflow:visible"
id="Arrow1Lend">
<path
d="M 0,0 L 5,-5 L -12.5,0 L 5,5 L 0,0 z "
transform="scale(-0.8,-0.8)"
style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;marker-start:none"
id="path5011" />
</marker>
</defs>
<g
id="layer1"
transform="matrix(0.15767037,0,0,0.15767037,36.993876,16.385944)"
inkscape:export-filename="layer1.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<path
d="m 800,500 a 300,300 0 1 1 -600,0 300,300 0 1 1 600,0 z"
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path1309" />
<path
d="M 50,500 H 350"
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path2184" />
<path
d="M 350,325 V 675"
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path2186" />
<path
d="M 350,400 550,250 V 50"
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path2188" />
<g
id="g1907">
<path
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 349.71875,594.96875 c -2.11823,0.0989 -3.94375,1.52309 -4.55502,3.55362 -0.61126,2.03053 0.12466,4.2258 1.83627,5.47763 l 100,75 100,75 c 1.42724,1.10231 3.33745,1.35074 4.99917,0.65017 1.66172,-0.70057 2.8175,-2.24161 3.02476,-4.03302 C 555.2312,748.82575 554.45784,747.06151 553,746 L 453,671 353,596 c -0.9374,-0.72093 -2.09997,-1.0863 -3.28125,-1.03125 z"
id="path3063" />
<path
id="path1913"
style="fill-rule:evenodd;stroke:#000000;stroke-width:8pt;marker-start:none"
d="m 486.928,702.696 -79.36,-9.52 48,-64 z" />
</g>
<text
x="34.285713"
y="460"
style="font-style:normal;font-weight:normal;font-size:12px;line-height:0%;font-family:'Bitstream Vera Sans';fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1"
id="text3938"
xml:space="preserve"><tspan
x="34.285713"
y="460"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:144px;line-height:125%;font-family:Arial;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-linecap:round"
id="tspan3940">B</tspan></text>
<text
x="570"
y="120"
style="font-style:normal;font-weight:normal;font-size:12px;line-height:0%;font-family:'Bitstream Vera Sans';fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1"
id="text3942"
xml:space="preserve"><tspan
x="570"
y="120"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:144px;line-height:125%;font-family:Arial;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-linecap:round"
id="tspan3944">C</tspan></text>
<text
x="570"
y="972.57141"
style="font-style:normal;font-weight:normal;font-size:12px;line-height:0%;font-family:'Bitstream Vera Sans';fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1"
id="text3946"
xml:space="preserve"><tspan
x="570"
y="972.57141"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:144px;line-height:125%;font-family:Arial;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-linecap:round"
id="tspan3948">E</tspan></text>
<path
d="M 550,750 V 950"
style="fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-start:none"
id="path5022" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
posts/img/bjt-again.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
posts/img/bjt-diagram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
posts/img/bjt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

50
posts/img/computer.svg Normal file
View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="284.27203mm"
height="284.27203mm"
viewBox="0 0 284.27203 284.27203"
version="1.1"
id="svg5"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs2" /><g
id="g4"><path
stroke="#848484"
d="M 79.951508,4.441751 H 239.85454 M 71.068008,13.325252 h 8.8835 m 151.019532,0 h 8.8835 M 62.184508,22.208754 h 8.8835 m 151.019522,0 h 17.76701 M 53.301008,31.092255 h 8.8835 m 159.903022,0 h 17.76701 M 53.301008,39.975756 h 8.8835 m 17.767,0 H 204.32053 m 17.767,0 h 17.76701 M 53.301008,48.859257 h 8.8835 m 159.903022,0 h 17.76701 M 53.301008,57.742759 h 8.8835 m 159.903022,0 h 17.76701 M 53.301008,66.62626 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.88351 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m -186.553532,8.8835 h 8.8835 m 159.903022,0 h 17.76701 m 8.8835,0 h 17.767 m -213.204032,8.8835 H 230.97104 m 8.8835,0 h 17.767 m -26.6505,8.8835 h 17.767 m 8.8835,0 h 8.8835 M 35.534008,173.22828 H 239.85454 m 8.8835,0 h 17.767 m -239.854532,8.8835 h 8.8835 m 204.320532,0 h 26.6505 m -239.854532,8.8835 h 8.8835 m 204.320532,0 h 26.6505 m -239.854532,8.8835 h 8.8835 m 97.718512,0 h 88.83501 m 17.76701,0 h 26.6505 m -239.854532,8.8835 h 8.8835 m 186.553522,0 h 53.30101 m -248.738032,8.8835 h 8.8835 m 26.6505,0 h 17.767 m 124.369022,0 h 8.8835 m 35.53401,0 h 8.8835 m 17.767,0 h 8.8835 m -257.621532,8.8835 h 26.6505 m 17.767,0 h 8.8835 m 17.76701,0 h 8.883502 m 17.767,0 h 8.8835 m 17.767,0 h 8.88351 m 17.767,0 h 8.8835 m 8.8835,0 h 79.95151 m -257.621542,8.8835 h 8.88351 m 8.8835,0 h 8.8835 m 17.767,0 h 8.8835 m 17.767,0 h 8.88351 m 17.767002,0 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.8835 m 8.8835,0 h 8.8835 m 62.18451,0 h 17.767 M 8.883498,244.29629 h 8.8835 m 8.88351,0 h 8.8835 m 8.8835,0 h 17.767 m 8.8835,0 h 17.767 m 17.767012,0 h 8.8835 m 8.8835,0 h 17.767 m 8.8835,0 h 17.76701 m 79.95151,0 h 26.6505 M 0,253.17979 h 8.883498 m 168.786532,0 h 8.8835 m 53.30101,0 h 26.6505 M 0,262.06329 h 8.883498 m 168.786532,0 h 79.95151 m -26.6505,8.8835 h 8.8835"
id="path1051"
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#7c6f64;stroke-opacity:1" /><path
stroke="#c6c6c6"
d="M 79.951508,13.325252 H 222.08753 M 71.068008,31.092255 H 222.08753 M 71.068008,39.975756 h 8.8835 m 133.252522,0 h 8.8835 M 71.068008,48.859257 h 8.8835 m 133.252522,0 h 8.8835 M 71.068008,57.742759 h 8.8835 m 133.252522,0 h 8.8835 M 71.068008,66.62626 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.88351 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 H 222.08753 m 35.53401,8.8835 h 8.8835 m -17.767,8.8835 h 8.8835 m -17.767,8.88351 h 8.8835 m -204.320532,17.767 H 239.85454 m -195.437032,8.8835 h 88.835012 m 88.83501,0 h 17.76701 m -195.437032,8.8835 h 17.767 m 17.767,0 h 53.301012 m -88.835012,8.8835 h 17.767 m 17.767,0 H 204.32053 m 8.8835,0 h 8.8835 m 17.76701,0 h 8.8835 m 17.767,0 h 8.8835 m -88.83501,35.53401 h 8.8835 m 35.53401,0 h 8.8835 M 8.883498,262.06329 H 177.67003 m 8.8835,8.8835 h 44.41751"
id="path1053"
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#a89984;stroke-opacity:1" /><path
stroke="#ffffff"
d="m 222.08753,13.325252 h 8.88351 M 71.068008,22.208754 H 222.08753 M 62.184508,31.092255 h 8.8835 m -8.8835,8.883501 h 8.8835 m 133.252522,0 h 8.8835 M 62.184508,48.859257 h 8.8835 m 133.252522,0 h 8.8835 M 62.184508,57.742759 h 8.8835 m 133.252522,0 h 8.8835 M 62.184508,66.62626 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.88351 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 133.252522,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 8.8835,0 H 213.20403 m -151.019522,8.8835 h 8.8835 m -35.534,35.53401 H 239.85454 m -204.320532,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m 177.670022,0 h 17.76701 m 17.767,0 h 8.8835 m -204.320532,8.8835 h 8.8835 m 17.767,0 h 8.88351 m 17.767002,0 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.8835 m -151.019522,8.8835 h 8.8835 m 17.767,0 h 8.8835 m 17.767,0 h 8.8835 m 17.767012,0 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.8835 m 26.6505,0 h 62.18451 m -239.854542,8.88351 h 8.88351 m 159.903022,0 h 62.18451 M 8.883498,253.17979 H 177.67003 m 17.767,0 h 35.53401"
id="path1055"
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#d4be98;stroke-opacity:1" /><path
stroke="#000000"
d="m 239.85454,13.325252 h 8.8835 m -8.8835,8.883502 h 8.8835 m -8.8835,8.883501 h 8.8835 m -8.8835,8.883501 h 8.8835 M 79.951508,48.859257 H 195.43703 m 44.41751,0 h 8.8835 M 79.951508,57.742759 h 8.8835 m 151.019532,0 h 8.8835 M 79.951508,66.62626 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.8835 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.8835 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.8835 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.88351 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.8835 h 8.8835 m 151.019532,0 h 8.8835 m -168.786532,8.8835 h 8.8835 m 151.019532,0 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -17.767,8.8835 h 8.8835 m 26.6505,0 h 8.8835 m -222.087532,8.8835 H 230.97104 m 35.534,0 h 8.8835 m -8.8835,8.88351 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -142.13602,8.8835 h 88.83501 m -168.786522,17.767 h 8.8835 m 17.767,0 h 8.8835 m 17.767012,0 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.8835 m 17.767,0 h 8.8835 m 79.95151,0 h 8.8835 m -239.854532,8.8835 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.883502 m 17.767,0 h 8.8835 m 17.767,0 h 8.88351 m 17.767,0 h 8.8835 m 88.83501,0 h 8.8835 m -248.738032,8.88351 h 8.8835 m 17.767,0 h 8.8835 m 17.767,0 h 8.88351 m 17.767002,0 h 8.8835 m 17.767,0 h 8.8835 m 17.76701,0 h 8.8835 m 97.71851,0 h 8.8835 m -17.767,8.8835 h 8.8835 m -17.767,8.8835 h 8.8835 M 8.883498,270.94679 H 177.67003 m 62.18451,0 h 17.767 m -71.06801,8.8835 h 53.30101"
id="path1057"
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#282828;stroke-opacity:1" /><path
stroke="#000084"
d="m 195.43703,48.859257 h 8.8835 m -8.8835,8.883502 h 8.8835 m -8.8835,8.883501 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.88351 h 8.8835 m -8.8835,8.8835 h 8.8835 m -8.8835,8.8835 h 8.8835 m -124.369022,8.8835 H 204.32053"
id="path1059"
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#2e3b3b;stroke-opacity:1" /><path
stroke="#0000ff"
d="M 88.835008,57.742759 H 195.43703 M 88.835008,66.62626 h 8.88351 m 8.883502,0 h 88.83501 m -106.602022,8.8835 h 8.88351 m 8.883502,0 h 88.83501 M 88.835008,84.39326 H 195.43703 M 88.835008,93.27676 H 195.43703 M 88.835008,102.16027 H 195.43703 m -106.602022,8.8835 H 195.43703 m -106.602022,8.8835 H 195.43703"
id="path1061"
style="fill:none;fill-opacity:1;stroke-width:8.8835;shape-rendering:crispEdges;stroke:#7daea3;stroke-opacity:1" /><path
stroke="#01feff"
d="m 97.718518,66.62626 h 8.883502 m -8.883502,8.8835 h 8.883502"
id="path1063"
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#87efcc;stroke-opacity:1" /><path
stroke="#008400"
d="m 62.184508,208.76228 h 17.767"
id="path1065"
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#c4de3d;stroke-opacity:1" /><path
stroke="#838383"
d="m 97.718518,244.29629 h 8.883502 m 71.06801,0 h 8.8835 m -8.8835,26.6505 h 8.8835"
id="path1067"
style="stroke-width:8.8835;shape-rendering:crispEdges;stroke:#7c6f64;stroke-opacity:1" /></g></svg>

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
posts/img/docker-net-ls.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
posts/img/elliott-900.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
posts/img/file-launcher.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
posts/img/gruvbox95.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
posts/img/ibm-pc.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
posts/img/keycaps.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
posts/img/lineage.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
posts/img/mk-worm.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
posts/img/nand-fork.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
posts/img/note-view.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
posts/img/obsidian-tags.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
posts/img/outside-tmoc.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
posts/img/reverse-proxy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
posts/img/timer-module.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
posts/img/toolbar-one.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
posts/img/toolbar-three.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
posts/img/toolbar-two.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
posts/img/tux-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
posts/img/tux.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

BIN
posts/img/waybar-full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
posts/img/zk-tags.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View file

@ -0,0 +1,442 @@
---
title: "Informal definitions of key concepts in propositional logic"
slug: /informal-concepts-propositional-logic/
date: 2021-11-27
tags: ["article"]
---
## Introduction
This post is the first of a series on propositional logic that introduces the
foundational logical concepts of validity, soundness, truth, falsity,
possibility and indeterminacy. These concepts are defined here informally and
will receive formal articulation in later posts.
## What is propositional logic?
The chief unit of propositional logic is the _proposition_. It is typically said
that the sentences of a language _express_ propositions. Thus, in natural
languages, propositions are expressed via declarative sentences: a statement
that _such and such is the case_. For example, in English: _snow is white_,
_London is the capital of the United Kingdom_, _John is travelling to Stockholm_
etcetera. Whilst this is true it is important not to assume that a proposition
reduces to its expression by a given sentence. There are numerous examples of
why this assumption proves problematic. For example, _il pleut_ and _it is
raining_ express the same proposition but do so via different sentences in
different languages. Similarly the semantically ambiguous English sentence _Two
cars were reported stolen by the police yesterday_ expresses two possible
propositions (the police reported the car stolen, the police stole the car) that
are left underdetermined by the surface grammar of the sentence. For simplicity
we will talk about propositions exclusively in terms of sentences but it is
important to note that the two are not straightforwardly interchangeable.
Not every sentence in a language has a propositional form. Consider for example
_Áchtung!_ or _thanks_. It is not obvious that these sentences express a
proposition in the manner of declarative sentences. Philosophers and linguists
have called expressions that do not satisfy propositional criteria
[speech acts](https://en.wikipedia.org/wiki/Speech_act). The difference between
declarative sentences and speech acts (like thanking someone or issuing an
order) centers on the fact that the former possess clear _truth-conditions_ that
reduce to a given _truth-value_. In the case of _il pleut_ the sentence is true
if it is raining and false otherwise. Those are its truth-conditions. Its
truth-value is the particular assignment that is made on the basis of these
conditions. Assume it is not raining; in this case the truth-value of _il pleut_
is false.
Although declarative sentences are a subset of the totality of possible
grammatical expressions in any natural language, they are clearly an important
subset. They form the basis of all scientific and mathematical discourse and are
our primary means of spreaking and reasoning about the world. The scope of
propositional logic is limited to sentences that have this declarative property.
There are no questions, commands or exhortations in propositional logic, only
statements which may be true or false.<sup>1</sup>
The purpose of propositional logic is to analyse propositions in terms of their
truth conditions and to derive rules governing their proper application and
combination in arguments. Equipped with these rules, we are able to demonstrate
for example that an argument is valid or that it displays sound reasoning. On
the other hand, the same rules will allow us to demonstrate when an argument is
invalid or that it leads to contradiction.
## Arguments and consistency
Propositional logic proceeds upon two interconnected axes:
<ol>
<li>Analysing compound propositions in terms of their constituent parts</li>
<li>Analysing a proposition in relation to other propositions</li>
</ol>
In this post we are focused on the second axis. The first will be covered in a
future post on logical connectives and truth-tables.
When we analyse a series of propositions and the relations between them we call
the group a _set_. Sets possess logical _properties_. The first such property we
will define is **consistency**.
> A given set of propositions is consistent if and only if it is possible for
> each member of the set to be true at the same time. It is inconsistent just if
> this is not the case.
The following set of propositions form an inconsistent set.<sup>2</sup> Can you
spot the inconsistency?
```
(1) Anyone who takes astrology seriously is crazy.
(2) Jane is my sister and no sister of mine has a crazy person for a husband.
(3) Richard is Jane's husband and he checks his horoscope every morning.
(4) Anyone who checks their horoscope takes astrology seriously.
```
The set is inconsistent because it is not the case that all the propositions can
be true at once. Specifically: if (1), (3), and (4) are true, (2) cannot be.
Alternatively, if (2), (3) and (4) are true (1) cannot be.
Let's illuminate the first instance of inconsistency. On the one hand we assert
that taking astrology seriously is crazy and we assume that checking your
horoscope means that you take astrology seriously. Richard is Jane's husband and
he checks his horoscope each morning. By definition then, Richard is crazy and
Jane is married to him, but if I believe this, I cannot believe that my sister
would not marry a crazy person. My beliefs are inconsistent.
Now, the second instance of inconsistency. Jane, my sister, is married to
Richard. None of my sisters have a crazy husband. Richard checks his horoscope
each day and therefore takes astrology seriously. If this is the case, I cannot
believe that taking astrology seriously is crazy because otherwise my sister
cannot be married to Richard.
In deconstructing the inconsistencies in the scenario of Richard and Jane we
have been concerned to point out that one proposition _implies_ or _follows_
another proposition. Intuitively we have been invoking the logical notion of an
**argument**. This is not so different from what we mean by an argument in
ordinary life. If we are arguing with someone, we believe that they are wrong
about something where 'something' is a proposition and 'wrong' means _false_.
For example _the prime minister is a liar_. A more logical way to put this is
that we believe their beliefs about a set of propositions are inconsistent. In
order to make assertions about the relative consistency or inconsistency of a
set of propositions we advance arguments. This is like seeking to change the
person's viewpoint by showing that their belief _A_ conflicts with their other
belief _B_ or that if _A_ is true, they cannot believe _B_.
In the example above each proposition in the set has equal footing; we have not
distinguished one type of proposition from any other. When we construct an
argument however, we distinguish the propositions by type. We say that one or
more propositions are _premises_ and one proposition is the _conclusion_:
> An argument is a set of propositions comprising one or more premises and a
> conclusion. The conclusion is taken to be supported by the premises.
Let's demonstrate how this works by making an implicit argument from previous
example explicit:
```
(P1) Anyone who checks their horoscope takes astrology seriously.
(P2) Richard checks his horoscope.
(C) Richard takes astrology seriously.
```
This constitutes a logical argument because of how the propositions are
arranged: we are asserting that the conclusion (C) is supported by premises P1
and P2. We call such arguments **syllogisms**.
## Evaluation criteria for arguments
In what sense do the premises support the conclusion of an argument? What is the
relation between these two types of proposition? The word 'support' is rather
vague. In logic there are different ways to assess the qualility of an argument:
**inductive strength** and **deductive validity**.
Consider the following argument:
```
(P1) When a cat scratches itself it can mean it has fleas.
(P2) Our tabby, Carrot has been scratching himself a lot lately.
(C) Carrot has fleas.
```
To ask ourselves whether this is a strong argument is to reflect on whether we
have good grounds for believing the conclusion given the premises. My intuition
that this is a reasonable argument but not a particularly strong one. It doesn't
strain credulity but it is by no means watertight.
Contrast it with this argument:
```
(P1) Every day the sun rises in the east.
(P2) The sun rose in the east today.
(C) The sun will rise in the east tomorrow.
```
This strikes me as a stronger argument than the first. If you had to bet on
Carrot having fleas or the sun rising in the East you would put your money on
the latter although you would probably get better returns on the former. With
arguments of this nature we are proceeding on the basis of likelihood. The
technical term for this is _induction_ : given some background context of
beliefs (for instance the typical behaviour of cats and the planet) there are
stronger or weaker grounds for accepting the conclusion of an argument bases on
its premises.
> An argument is inductively strong if and only if the conclusion is probably
> true given the premises.
Although the arguments differ in their relative strength they are both inductive
arguments. This is because they are each falsifiable. In the first case, this is
obvious: Carrot might not have fleas and could be scratching for some other
reason. Perhaps surprisingly, the second argument is also falsifiable. The
magnetic field of the Earth could switch polarity meaning that while the Earth
would not change its position relative to the sun, our compass would be inverted
and therefore indicate that the sun rising in the west. This is very unlikely to
happen imminently but it _will_ happen at some point in the next 10,000 years.
Therefore the conclusion could prove false.
The next obvious is whether all arguments are like this. Is probability the best
we can hope for? Fortunately not. Propositional logic is a deductive schema
which means it aims for truths that are not falsifiable in the manner of the two
examples above. It is this criterion of evaluation that we mainly interested in
when we use logic as a formal discipline. This is the domain of deductive
validity. Validity is our second key logical property.
> An argument is deductively valid if and only if it is not possible for the
> premises to be true and the conclusion false.
## Validity and soundness
The following syllogism is an example of a valid argument:
```
(P1) All fish live in the sea and only in the sea.
(P2) Cod are fish.
(C) Cod live in the sea.
```
And here is an invalid argument:
```
(P1) All fish live in the sea and only in the sea.
(P2) Cod are fish.
(C) Cod live on land.
```
In the valid instance, there is no sense to the idea that we might accept each
premise and yet deny the conclusion. In the invalid instance this is not the
case: we can accept each premise and deny the conclusion. We can relate this
back to the the notion of consistency. Recall that a set of propositions is
consistent if and only if it is possible for each member to be true at once.
With the first argument we cannot consistently accept the premises and deny the
conclusion whereas in the case of the second argument we can quite consistently
accept the premises and deny the conclusion since the propositions do not
comprise a consistent set.
In contrast to the previous inductive arguments, with valid arguments the
conclusion is supported purely in virtue of the terms used in the premises and
the propositions they express. In order to assess the validity of an argument
like the first it is not neccessary to aquaint oneself with cod and to study
their behaviour so as to determine whether they do in fact live in the sea. All
that is necessary is to understand the propositions expressed. Philosophers
refer to statements of this sort as _analytic_. They are true or false 'by
definition'. More specifically: the concept of the predicate is contained within
the concept of the subject for example _all brothers are male_. In the case of
the argument, we have defined at (P1) that fish are creatures that live in the
sea so given this definition, the conclusion is bound to follow from the
premises since it is just a specific instance of the general property already
defined. Validity is therefore an entirely formal notion that exists over and
above any facts of the matter. If I have defined 'fish' universally as
sea-dwellers I cannot without inconsistency say that they are not sea-dwellers.
This is further exemplified with an argument like the following:
```
(P1) Manchester is the capital of the UK.
(P2) Manchester is north of Birmingham.
(C) The capital of the UK is north of Birmingham.
```
Is this a valid argument? To answer this question remember that invalidity means
that it is possible for the premises to be true and the conclusion false. In the
strict logical sense, this is valid argument since _were_ the premises true, the
conclusion would also be true. The point is that validity is a function of
truth-conditions not truth-values. The truth value of the first premise and the
conclusion in the above argument happen to be false but this does not affect its
validity. There is no necessity to London being the capital of the UK and not
Manchester. We can imagine things being otherwise which is to say that we can
entertain the truth-conditions of the proposition and make judgements in
accordance with it being true or false quite independently of whether it is in
actuality true/false.
We can take this back to the earlier example of the invalid argument about cod:
in order to judge the argument invalid it was not necessary for us to look for
cod that live on land and come back empty. Rather we just had to assume that if
all fish live in the sea then it must be the case that if something is a fish,
it is a sea-dweller. We made no commitment to fish actually living in the sea.
Does this mean that actual truth does not matter to logic? No, it just means
that validity as a property is decoupled from truth as a property although we
cannot of course have a grasp of the notion of validity without possessing a
prior notion of truth. A proposition being true in fact is a property it may or
not possess in addition to its membership within a valid sequence of reasoning.
If an argument is both valid and its premises are true in fact we say that it is
a **sound** argument. This is a stronger criterion of evaluation than validity
alone.
> An argument is sound if and only if it is deductively valid and all of its
> premises are true.
It follows from this definition of soundness that:
- an argument cannot be sound if it is not also valid
- an argument can be valid without being sound
- if an argument is sound its conclusion must be true
(The last point follows from the fact that soundness means the premises are true
and validity requires that if the premises are true the conclusion must also be
true.)
We have already seen examples of arguments that are valid but not sound in the
Manchester example, let's close this section with an example where both premises
and conclusion are true yet the argument is invalid, demonstrating that truth
alone is not sufficient for soundness.
```
(P1) London is the capital of the UK.
(P2) The capital of the UK is in the southern part of the country.
(P3) Cambridge is not the capital of the United Kingdom
(C) London is south of Cambridge
```
This argument is deductively invalid because we can consistently assert the
premises but deny the conclusion. Specifically: there isn't anything about the
premises that makes the denial of the conclusion inconsistent. From the point of
view of the premises alone, London could be north of Cambridge whilst still
being in the southern part of the country.
## Logical possibility
In distinguishing the properties of logical consistency and validity we have
been making much tacit use of the notion of _possibility_. This is because when
we consider the validity of an argument we are assessing truth-conditions and
this consists in asking ourselves what could or could not be the case: were it
such that _P_, then it would be the case that _Q_. It is important to understand
what possibility means in the context of logic and how it differs from what we
might mean ordinarily when we use the term.
It is evident from the case of arguments that are valid but not sound that logic
operates with a specialised notion of possibility. For example it has to be the
case that the proposition _Every woman can levitate_ is logically possible since
the following argument is valid:
```
(P1) Ellen is a woman.
(P2) Every woman can levitate.
(C) Ellen can levitate.
```
But we know of course that women cannot levitate. When we assert that this is
impossible we are relying on a stronger notion of possibility than logical
possibility. It follows that the concept of possibility can have different
degrees. The scope of the concept of possibility has been the concern of
logicians and philosophers since at least the time of Plato and numerous
different formulations exist. The notion that we mostly work with unreflectively
in everyday life is nomological possibility. This means 'governed by the
application of laws' where these laws pertain to our current understanding of
the natural world as determined by physics. Levitation is therefore
nomologically impossible but logically possible.
If logical possibility is not contrained by the laws of physics does it place
any restrictions on what is possible? Logic applies a single restriction, the
law of non-contradiction: a proposition cannot both be true and false at once.
The following propositions are examples of a contradictory propositions.
```
There is a dog that is not a dog.
Today is Tuesday and today is not Tuesday.
The cat that is dead is alive.
```
From this we can derive the following property of logical possibility:
> A proposition is logically possible just if it does not imply a contradiction.
## Logical truth, falsity and indeterminacy
What are the truth-conditions of a contradictory proposition? We know that a
logically possible proposition such as _every woman can levitate_ could be true
or false. It has to be so because we are capable of constructing valid arguments
where it features as a premise and a valid argument implies the possible truth
of its premises.
In the case of a contradiction there are no conditions under which it could be
judged to be true. For this reason, contradictions are classified as **logically
false**. This is distinct from ordinary falsity where a proposition _could_ be
true but happens to be false. Logically false propositions are universally false
and could never be true. This is consistent with our previous observation of the
law of non-contradiction: if a proposition cannot be both true and false at once
we are saying that something _cannot be the case_ which is of course to say _is
false_.
Logical falsity is therefore another property that a proposition may possess and
it is a property that is possessed by all propositions that are contradictions:
> A proposition is logically false if and only if it is not possible for the
> sentence to be true.
Complementing logical falsity is **logical truth**:
> A proposition is logically true if and only if it is not possible for the
> sentence to be false.
We call logically true propositions tautologies. Some examples:
```
An apple is an apple.
Today is Tuesday or today is not Tuesday.
The cat is dead or alive.
```
The properties of logical truth and falsity are alike in their universality.
Propositions that are logically true do not exclude any possibility (today is
Tuesday or it is not Tuesday; there is no possible state outside of this)
whereas logically false propositions exclude all possibilities (there is no
scenario where today is both Tuesday and not Tuesday).
We class all propositions that are not contradictions or tautologies **logically
indeterminate** propositions. This means that their truth-value is not assigned
purely on the basis of the meanings of the terms of which they are comprised.
_It is raining_ for example, is logically indeterminate because we cannot know
its truth-value just by reflecting on the meaning of the predicate _is raining_.
It may be true under certain conditions and false under others and in order to
know the specific truth-value at a given moment, we must look to states of
affairs beyond the sentence. The vast majority of propositions expressed in
natural and formal languages are indeterminate in this manner.
## Summary
In this post we introduced propositions as descriptions of states of affairs
that possess truth-conditions. We noted that propositions are expressed in
language through the medium of declarative sentences and that not every
expression in a language possesses a propositional form. Two key properties that
pertain to sets of propositions were introduced and exemplified: consistency and
validity. In addition we considered different evaluative criteria for logical
arguments comparing inductive strength with deductive validity. We distinguished
logical possibility from nomological possibility and explained how the law of
non-contradiction places bounds on what is logically possible. Equipped with the
concept of logical possibility we were able to introduce logical truth and
falsity, analysing the truth-conditional form of tautologies and contradictions
and noting that propositions that are neither logically true or false are
logically indeterminate.
## Notes
<ol>
<li>These all being standard instances of speech acts which are resistant to truth-conditional analyses as noted above.</li>
<li>This example comes from (Bergmann, Moor and Nelson, 2014).</li>
</ol>
## References
Bergmann, M., Moor, J. and Nelson, J. (2014). The logic book. Boston:
Mcgraw-Hill/Connect Learn Succeed.
Wikipedia Contributors (2019). Speech-act. [online] Wikipedia. Available at:
https://en.wikipedia.org/wiki/Speech_act.

View file

@ -0,0 +1,157 @@
---
title: "Jest parameterization"
slug: /jest-parameterization/
date: 2023-11-20
tags: ["learning", "javascript", "unit-testing"]
---
At work I am in the process of upgrading our AWS Lambdas to use the v.18 Node
runtime (most of them are still using v.14). It has been a good opportunity to
carry out refactoring and address technical debt.
Many of the lambdas lack unit tests whilst others have tests that haven't been
maintained. Thus I've been spending time adding and optimising tests in Jest.
I've been harnessing parameterization when improving unit test coverage. The
lambdas I'm currently working on are subroutines within a broader
[AWS Step Function](https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html)
that constitutes the backend of one of our internal content management systems.
Much of the functionality consists in generating and parsing properties from XML
and JSON. As I am testing the same code under different conditions, the tests
are highly repetitive and can be readily parameterized.
Here's one example:
```js
describe("handler", () => {
let mockApiGatewayEvent = {
propertyId: "1234",
isCrossPublished: false,
}
describe("exit conditions", () => {
it("should throw an error if a user attempts to unpublish an alpha file", async () => {
event = {
...event,
fileId: "alpha:1234",
}
await expect(handler(event)).rejects.toThrow(
"Not allowed to unpublish file alpha:1234"
)
})
it("should throw an error if user attempts to unpublish a beta file", async () => {
event = {
...event,
fileId: "beta:1234",
}
await expect(handler(event)).rejects.toThrow(
"Not allowed to unpublish file beta:1234"
)
})
// and so on...
})
})
```
I've anonymised the specifics of the data but the process is straightforward:
I'm asserting that the correct error text is returned if a user attempts to
delete a certain filetype. I reduced the verbiage of countless `it` blocks by
utilising parameterization:
```js
describe("exit conditions", () => {
let mockApiGatewayEvent = {
projectId: "1234",
preview: false,
}
it.each([["alpha:1234"], ["beta:1234"]])(
"should throw an error if user attempts to unpublish %s file",
async (fileId) => {
mockApiGatewayEvent = {
...mockApiGatewayEvent,
fileId,
}
await expect(handler(mockApiGatewayEvent)).rejects.toThrow(
`Not allowed to unpublish file ${fileId}`
)
}
)
})
```
Instead of multiple `it` clauses, there is a single `each` expression that loops
through each file variant, executing the same test each time, changing only the
file name that is output.
The `%s` symbol is a placeholder for string substitution. This lets me include
the name of each variant in the test description. This is important because
there is a risk of obscuring the specifics of each test iteration when using
parameterization. It's essential to be able to trace a failure to the specific
iteration.
In the example below, the process is functionally the same but there are more
parameters in the mix:
```js
describe("deletePageFromS3()", () => {
beforeEach(() => {
process.env.AWS_ENV = "live"
s3ClientMock.reset()
})
const parameters = [
{
previouslyPublished: false,
isDraft: false,
bucket: "bucket:alpha",
key: "key:alpha",
},
{
previouslyPublished: true,
isDraft: false,
bucket: "bucket:beta",
key: "key:beta",
},
{
previouslyPublished: false,
isDraft: true,
bucket: "bucket:gamma",
key: "key:gamma",
},
{
previouslyPublished: true,
isDraft: true,
bucket: "bucket:delta",
key: "key:delta",
},
]
it.each(parameters)(
"should return page for deletion, given: previouslyPublished is %s, isDraft is %s",
async ({ previouslyPublished, isDraft, bucket, key }) => {
await deleteFileFromS3("url", previouslyPublished, isDraft)
const deleteObjectCommand = s3ClientMock.calls()[0].args[0]
expect(deleteObjectCommand.input).toEqual({
Bucket: bucket,
Key: key,
})
}
)
})
```
The process under test is a function that uses the AWS SDK to delete objects
from an S3 bucket. I am checking that the (mocked) S3 client is called with the
correct parameters.
This time I am passing in an array of objects to the `each` function rather than
a multi-dimensional array. This makes it easier to destructure the specific
properties in the individual test cases. Again I use `%s` to interpolate a
subset of the parameters into each test description. `%s` applies to the each
value in the `each` array in sequence so you just repeat it to individuate the
different params.
This has been a brief sketch of some applied examples of parameterization. For a
better account, see
[Parameterized tests in JavaScript with Jest](https://blog.codeleak.pl/2021/12/parameterized-tests-with-jest.html)
by Rafał Borowiec.

View file

@ -0,0 +1,166 @@
---
title: "Getting a local DynamoDB instance working"
slug: /local-dynamodb-setup-sam/
date: 2024-06-08
tags: ["log", "aws", "dynamodb"]
---
I wasted most of an afternoon trying to get the following set-up:
- Two local instances of DynamoDB running in Docker
- The ability to add table data to either instance via NoSQL Workbench and query
it via an AWS lambda
If you are coming from SQLPro or DBeaver, Amazon's NoSQL workbench is a bit
unintuitive. Creating the lambdas and the Docker containers was easy, it was
getting them recognised by this client that was tricky. Anyway this is the
process.
I want to set up two Docker instances of the `amazon/dynamodb-local` image,
along with a bridging network so that the Docker container that SAM runs in can
communicate with the containers that DynamoDB runs in. Hence `docker-compose` is
the way to go:
```yml
# docker-compose.yaml
version: "3.8"
services:
dev:
image: amazon/dynamodb-local
container_name: timetracking_dynamodb_dev
ports:
- "8000:8000"
volumes:
- "./data:/home/dynamodblocal/data"
networks:
- sam-local
stage:
image: amazon/dynamodb-local
container_name: timetracking_dynamodb_stage
ports:
- "8001:8001"
volumes:
- "./data:/home/dynamodblocal/data"
networks:
- sam-local
networks:
sam-local:
driver: bridge
```
My two DDB instances are `dev` and `stage` running on ports 8000 and 8001
respectively. Their shared network is called `sam local`.
After running `docker-compose up`. I run `docker network ls` and it confirms the
network has been created (it prepends the network name with the repo name):
![](./img/docker-network-ls.png)
Next comes SAM. I've just created the default Typescript Lambda template using
the SAM CLI. For now this will only expose a single API Gateway endpoint,
`/fetch`, which I will use to GET my DynamoDB table data. The key info from the
template:
```yml
# template.yaml
Resources:
TimeTrackingFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Handler: index.handler
Runtime: nodejs20.x
Architectures:
- x86_64
Events:
Fetch:
Type: Api
Properties:
Path: /fetch
Method: get
```
I'll use the following command to start the local APIGateway server:
```sh
sam build && sam local start-api --docker-network time-tracking_sam-local
```
If I run `docker network inspect time-tracking_sam-local`, I can confirm that
the SAM Docker instance (which AWS creates and which is effectively a black box
to me) is on the same network as the two DDB instances that I set up earlier:
![](./img/docker-network-inspect.png)
Next I want to view my containers in NoSQL workbench so I can create the table
schema and start adding data. I'm just going to worry about the `dev` container
here. I go to the _Operation builder_ and add a new connection:
![](./img/workbench-create-conn.png)
This automatically generates an _Access key ID_ and _Secret access key_ even
though this is a local service that won't interact with the production version
in any way. You _must_ use these. Some guides say you can put anything when
connecting locally but in my experience this is _not true_.
Using these keys, I write a skeletal lambda just to check the connection via the
AWS SDK for DynamoDB:
```ts
import { DynamoDBClient, ListTablesCommand } from "@aws-sdk/client-dynamodb"
const client = new DynamoDBClient({
region: "localhost",
endpoint: "http://dev:8000",
credentials: {
accessKeyId: "xxxx",
secretAccessKey: "xxxx",
},
})
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
const command = new ListTablesCommand({})
const response = await client.send(command)
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: "Successfully retrieved tables",
tableNames: response.TableNames,
}),
}
...
```
The first problem because I wasn't providing a region. Why would I, this is
local? But it needs to be `localhost`. And again, you must put the actual
credential values that the Workbench gives you.
The next issue was that this function was returning an empty array when I
queried the `/fetch` endpoint. Even though I had set up a table in Workbench:
![](./img/workbench-data-modeller.png)
You must click _Commit to Amazon DynamoDB_ even though this is counter-intuitive
and suggests you are going to auto-provision paid services! You can then select
the local instance that you are going to seed with the table from the model:
![](./img/commit-table-workbench.png)
Now if you back to _Operation builder_, the table is associated with the given
instance and the data is displayed:
![](./img/ddb-data-in-builder.png)
On reflection, this makes a lot of sense as it decouples the data model from any
particular instance (local or production) that uses the data and makes it easy
to seed different tables to different database instances.
So finally, if I now query the `fetch` endpoint, I get the table name returned,
demonstrating that all the different parts have joined up:
![](./img/httpie-showing-data.png)

View file

@ -0,0 +1,756 @@
---
title: "N2T Unit One: Boolean functions and gate logic"
slug: /nand-to-tetris-unit-one/
date: 2023-01-13
tags: ["article", "learning", "computer-architecture"]
---
I have recently started [Nand to Tetris](https://www.nand2tetris.org/course).
This course teaches the foundations of hardware and computer achitecture and is
based on the textbook _The Elements of Computing Systems_ by Noam Nisan and
Shimon Schocken. As the course proceeds you build a functioning general-purpose
computer that is eventually capable of running Tetris and any number of other
programs. The hardware is built primarily through simulation software running on
Hack, a simplified
[hardware description language](https://en.wikipedia.org/wiki/Hardware_description_language)
similar to Verilog and VHDL.
In this post I outline what I learned in the first unit. This broadly follows
the curriculum but adds extra details I have acquired elsewhere to give the
fullest account.
## Bits and functions
The workings of a classical computer can be reduced to a series of operations on
the binary digits (bits) 0 and 1. A computational process can be represented as
a function: data (a series of bits) enters the function in one state and exits
in another state. This new state is a product of the function.
![](./img/function-diagram-new.png)
The most primitive bit operations are equivalent to the truth-conditions of the
logical connectives of Boolean algebra. In logic, the Boolean values are _true_
and _false_ but we will use 1 and 0, to represent these states as bits. There
are multiple logical connectives but we will mostly focus on AND, OR, and NOT
for simplicity.
The logic of each Boolean connective can be expressed as a function.
NOT can be represented as follows;
$$
f(x) = \lnot (x)
$$
NOT (¬) is a unary operator which means it takes one operand (_x_). We pass a
single bit as the operand and the function inverts its value. We can utilise
logical [truth tables]() to represent all possible inputs and outputs for the
operator:
| x | f(x) = NOT(x) |
| --- | ------------- |
| 1 | 0 |
| 0 | 1 |
If NOT receives 0 as an input it will return 1 as the output. If it receives 1
as the input it will return 0 as the output.
AND ($\land$) and OR($\lor$) are binary operators: they receive two operands as
input. Therefore we pass two bits to the function. As a bit can be one of two
values (0 or 1), there are four possible inputs.
For AND this gives us:
| x | y | f(x) = x AND y |
| --- | --- | -------------- |
| 1 | 1 | 1 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 0 | 0 | 0 |
AND returns 1 if both input bits are 1, otherwise it returns 0.
OR returns 1 if one or both bits are 1, otherwise it returns 0:
| x | y | f(x) = x OR y |
| --- | --- | ------------- |
| 1 | 1 | 1 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 0 | 0 | 0 |
## Logic gates
The logical function of each of the Boolean operators is implemented at the
level of computer hardware by logic gates. Each gate is represented with one or
more input pins and a single output pin through which the bits enter and exit.
The pins feed into and out of a chip which executes the function.
The diagram below shows the logic gates for AND, OR, and NOT.
![](./img/and-or-not-gates.png)
Logic gates are an abstraction. In reality, you cannot _see_ a logic gate in a
computer. If we were to look inside a chip that implements gate logic we would
see an arrangement of transistors, capacitors and resistors connected by wires.
These components are configured to mimic the behaviour of the logical operators.
_Nand to Tetris_ starts at the level of gates and does not discuss how the
individual gates are realised in electronic circuits. However, I think it is
useful to understand the basics of the electrical engineering so we might begin
to grasp how it is possible to go from an inert block of metal and silicon to
fully functioning computer.
### The electronic implementation of gate logic
We can start with the concept of a switch. Consider what is happening when we
turn on a light using a wall switch.
When the switch is off, the electrical circuit that connects the bulb to the
voltage source is broken. As a result, there is no potential difference between
the terminals and the current cannot reach the bulb. When the switch is on, the
circuit is complete and the current flows freely to the bulb.
This is represented by the following simple schematic:
![](./img/single-switch-and-circuit.png)
This circuit embodies the logic of a NOT gate. Think of the light as 1 and the
absence of light as 0. When the switch is on, 1 is the output and 0 is inverted.
When the switch is off, 0 is the output and 1 is inverted.
This scenario can be developed to represent the logical behaviour of an AND
gate. Imagine now that this room is a bit strange and there are two switches
controlling the operation of the bulb. If both switches are off the bulb will
not emit light. If one of the switches is on and the other is off the bulb will
not emit light. The bulb will only emit light when _both_ switches are on.
The schematic for this scenario embodies the logic of the Boolean AND
connective:
![](./img/double-and-circ.png)
Switch-controlled circuits are functionally equivalent to what actually happens
inside a computer when logical conditions are implemented via gates. Electrical
charge is directed along different routes depending on the value of an on/off
condition. However, in modern computers the actual component that controls the
flow of current is not a switch.
Whilst we could construct primitive computers with switches, the average CPU has
more than a million logic gates. Controlling this number of gates with
mechanical switches would be practically impossible and even if it were
achieved, it would result in extremely slow processing times.
This is an important point because the computational power of logic gates
emerges from their behaviour at scale. Collections of gates are combined to
express complex logical conditions that are a function of their individual
parts. For this to be possible, the output of a collection of gates needs to be
able to be fed into another collection and this it would be difficult to achieve
in a mechancial switch-based system.
Instead of mechanical switches, computers use transistors. Transistors are
semi-conductors: components that possess an electrical conductivity between that
of a conductor and an insulator. This property means they can both impede and
expede the flow of electrical charge.
There are different types of transistors but we will focus on basic Bipolar
Junction Transistors:
![](./img/bjt-again.png)
Applying a small amount of current at the base terminal (B) of a BJT allows a
larger current to flow from the collector (C) to the emitter (E). Removing
current at the base terminal reduces the flow from collector to emitter. This is
because the the emitter and collector are composed of a semi-conductor that has
a surplus of electrons (negatively charged) whereas the base has a deficiency of
electrons (positively charged). This state creates a modifiable potential
difference, reducing or increasing the current based on the voltage.
The base terminal of a transistor is another way of implementating the gate-like
behaviour we previously achieved with mechanical switches. However, there is an
important difference. With a switch, the circuit is actually broken when it is
in the "off" state and there is no current flowing at all. With a transistor,
the current drops in the "off" state but a voltage remains.
Because a continuous circuit is an analogue system, the quantities of
resistance, voltage and current are not discrete values, they will vary over a
given range. Thus "off" corresponds to "low" voltage and "on" corresponds to
"high" voltage. The specific stipulation will depend on the circuit design but
it is typically the case that a state of 1 or "on" is within the range 2-5V
whereas a state of 0 or "off" is within the range 0.0 - 0.8V.
## Boolean function synthesis
To recap, elementary computational processes can be represented as logical
functions. A function consists in one or more Boolean operators processing bits.
For each Boolean operator we can construct a chip that represents its truth
conditions. We call these chips logic gates. Logic gates are built with
transistors that either block or permit the flow of electric current. This makes
them behave in a manner almost identical to mechanical switches.
Now that we know how the individual logic gates work and how they are
implemented electronically, we will explore how they can be applied in
combination to represent complex logical states that more closely resemble
actual computer programs. This process is known as **Boolean function
synthesis**.
We will construct a logic circuit that represents the truth conditions for the
following state of affairs:
> The team plays on either Monday or Thursday and not at weekends
Let's call this _P_ for ease of reference.
This complex comprises several simpler atomic expressions:
<ol type="a" start="24">
<li>The team plays on Monday</li>
<li>The team plays on Thursday</li>
<li>The team plays at weekends</li>
</ol>
The first step is to construct a truth table. On the left-hand side we list all
the possible truth values for each individual expression. On the right-hand
side, we assign an overall truth value for their combination, based on whether
or not they reflect the truth conditions for _P_.
| x | y | z | _P_ |
| --- | --- | --- | --- |
| 1 | 1 | 1 | 0 |
| 1 | 1 | 0 | 1 |
| 1 | 0 | 1 | 0 |
| 1 | 0 | 0 | 1 |
| 0 | 1 | 1 | 0 |
| 0 | 1 | 0 | 1 |
| 0 | 0 | 1 | 0 |
| 0 | 0 | 0 | 0 |
We are only interested in the cases where _P_ is true, so we can discount any
lines that result in a truth value of 0 for the complex expresssion. This leaves
us with:
| x | y | z | _P_ |
| --- | --- | --- | --- |
| 1 | 1 | 0 | 1 |
| 1 | 0 | 0 | 1 |
| 0 | 1 | 0 | 1 |
Parsing each line, the truth table tells us that our complex expression (_P_) is
true in the following scenarios:
- If the team plays on both Mondays and Thursdays but not at weekends
- If the team plays on Mondays but not on Thursdays and not at weekends
- If the team plays on Thursdays but not on Mondays and not at weekends
We can formalise each case:
- _(x AND y) AND NOT z_
- _(x AND NOT y) AND NOT z_
- _(NOT x and y) AND NOT z_
We now have three logical expressions that if constructed with logic gates would
result in a partial representation of _P_. The representation would be partial
because each individual expression only conveys a single aspect of the truth of
_P_, not its totality. For example, if we constructed a circuit that represents
_(x AND NOT y) AND NOT z_, this would only cover occasions where the team plays
on Mondays but not on Thursdays (or the weekend). It wouldn't cover the case
where the team plays on Thursdays but not Mondays (or the weekend).
We want a circuit that captures all possible instances where _P_ returns true.
There are practical benefits to seeking a single implementation. In order to
maximise our computational resources we want to use the minimum number of gates
in the simplest configuration possible.
We start by concatenating each individual expression into a single disjunctive
expression using logical OR:
$$
((x \land y) \land \lnot z) \lor ((x \land \lnot y) \land \lnot z) \lor ((\lnot x \land y) \land \lnot z)
$$
Next, we look for opportunities to simplify this complex expression. This is
similar to simplifying equations in mathematical algebra. It is a heuristic
process; there is no formal or automated procedure that will work in every case.
_NOT z_ occurs in each of the individual disjunctive expressions. Therefore we
can reduce the repetition by using it only once:
$$
(x \land y) \lor (x \land \lnot y) \lor (\lnot x \land y) \land \lnot z
$$
Now we need to consider how we can simplify the remaining expressions:
$$
(x \land y) \lor (x \land \lnot y) \lor (\lnot x \land y)
$$
If we look closely we can see that this expression is displaying the truth
conditions for OR. The truth conditions for _x_ and _y_ are:
- true if _x_ and _y_ are true
- true if _x_ is true and _y_ is false
- true if _x_ is false and _y_ is true
This recalls our earlier definition of OR:
| x | y | f(x) = x OR y |
| --- | --- | ------------- |
| 1 | 1 | 1 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 0 | 0 | 0 |
Thus we can reduce:
$$
(x \land y) \lor (x \land \lnot y) \lor (\lnot x \land y)
$$
to:
$$
x \lor y
$$
The reduction is now complete, allowing us to reduce:
$$
((x \land y) \land \lnot z) \lor ((x \land \lnot y) \land \lnot z) \lor ((\lnot x \land y) \land \lnot z)
$$
to:
$$
(x \lor y) \land \lnot z
$$
If we construct a truth table for the original expression (_P_) and its
simplification (_P'_) we see that they are true under the same logical
conditions which demonstrates their equivalence:
| x | y | z | _P_ | _P'_ |
| --- | --- | --- | --- | ---- |
| 1 | 1 | 0 | 1 | 1 |
| 1 | 0 | 0 | 1 | 1 |
| 0 | 1 | 0 | 1 | 1 |
### Constructing the digital circuit
Now that we have reduced _P_ to its simplest form using the connectives AND, OR
and NOT we can construct a circuit using the logic gates for these connectives
to represent the overall state of affairs expressed by _P_.
We will have three input bits which correspond to _x_, _y_, _z_, and a single
output bit that will reflect the truth value of _P_ based on the inputs. The
input bits will be fed into an arrangement of logic gates that that matches the
logical connectives in _(x OR y) AND NOT z_.
<div style="display:flex;margin-top:1.5rem">
<iframe src="https://circuitverse.org/simulator/embed/nandtotetris-blog-post?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
</div>
We can confirm that the circuit implementation is an accurate representation of
_P_ by toggling the input values to confirm that the output is only 1 when
either _x_ or _y_ is true and _z_ is false.
### Further simplification with NAND
Our circuit uses three different types of logic gate. This is satisfactory but
it would better if we could simplify the circuit even further and use a single
gate rather than three. To do so we need to further reduce our logic and
introduce another type of logic gate: NAND.
NAND stands for "NOT AND" and its truth conditions are the inversion of AND:
| x | y | f(x) = x NAND y |
| --- | --- | --------------- |
| 1 | 1 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 0 | 0 | 1 |
NAND returns 1 whenever _x_ and _y_ are not both true. We will represent NAND in
our formulae with the symbol:
$$
\tilde\land
$$
NAND is a _universal logic gate_. This means that by using NAND gates and only
NAND gates, we can represent the truth function of every other logic gate (AND
can be expressed with just NAND gates, as can OR and so on). It follows that we
can create every possible logical circuit using NAND gates alone.
Let's demonstrate this by reformulating _(x OR y) AND NOT z_ with just NANDs:
$$
( [(x \tilde\land x) \tilde\land (y \tilde\land y)] \tilde\land (z \tilde\land z) ) \tilde\land ( [(x \tilde\land x) \tilde\land (y \tilde\land y)] \tilde\land (z \tilde\land z) )
$$
This is quite difficult to parse, so let's look at the circuit representation
and derive its equivalence to _(x OR y) AND NOT z_:
<iframe src="https://circuitverse.org/simulator/embed/nand-simplification?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
</div>
You will notice that there is repeated forking pattern to most of the inputs.
This occurs when the same input value is used for both input pins, equivalent to
_x NAND x_ in the equation:
![](./img/nand-fork.png)
When a NAND is wired to receive the same value for each input, it embodies the
truth conditions for NOT: in the diagram above when _a_ is 1 the output is 0 and
when _a_ is 0 the output is 1.
If we feed two of these sub-circuits into a NAND, we observe that the output is
consistent with the truth conditions for OR:
<div style="display:flex;margin-top:1.5rem">
<iframe src="https://circuitverse.org/simulator/embed/or_with_nand?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
</div>
This is equivalent to the _(x NAND x) NAND (y NAND y)_ section of our NAND
equation which we can see is equivalent to _x OR y_.
The final part of the NAND equation is dedicated to: _NOT z_. This is achieved
by using another forking NAND and applying it to _z_, this is then joined with
the existing fragment via a NAND to give:
$$
( [(x \tilde\land x) \tilde\land (y \tilde\land y)] \tilde\land (z \tilde\land z) )
$$
Which is then itself forked into a NAND to give the final output.
This is harder to parse than the implementation that used three different
operators but the point is just to demonstrate that such a reduction is possible
and that complex abstract states can be constructed from the concatenation of
primitive electronic components.
## Hardware Description Language
Digital circuits can be designed using a Hardware Description Language (HDL) and
simulation software. An HDL is a declarative programming language used to
describe the behaviour and structure of digital circuits. In _Nand To Tetris_
the HDL is Hack, a simplified HDL for teaching purposes.
An HDL file uses specialised syntax to describe the function and implementation
of a given chip. When it is fed into a simulator, we can test the chip's outputs
against a variety of inputs to check it is working as intended.
Below is an HDL specification file for the NAND logic gate written in Hack:
```
CHIP Nand {
IN a,b;
OUT out;
PARTS:
And(a=a,b=b,out=w);
Not(in=w1,out=out);
}
```
The code contains two sections:
- the interface (`CHIP`, `IN`, `OUT`)
- the implementation (`PARTS`)
The interface names the chip and designates its input and output pins. In the
example, the interface specifies two input pins (`a` and `b`) and a single
output pin (`out`).
The interface abstracts the actual implementation of the chip. It only tells us
the inputs and output, not how the output is generated from the input. This is
provided by the implementation section which details the internal workings of
the chip.
The NAND implementation invokes two other gates, AND and NOT. We are simply
taking the output of AND and inverting it with NOT. The HDL specification
describes the following circuit:
![](./img/nand-with-and-not.png)
Having defined the gate we can load it into the simulator and test its
behaviour.
![](./img/hardware-simulator-three.png)
We can change the values of the input pins and observe how this affects both the
output and the interim outputs of the implementation (ie. _w1_).
To be more efficient we can create a test file that runs through all our
expected outputs:
```
# Nand.tst
load Nand.hdl,
output-file Nand.out,
compare-to Nand.cmp,
output-list a%B3.1.3 b%B3.1.3 out%B3.1.3;
set a 0,
set b 0,
eval,
output;
set a 0,
set b 1,
eval,
output;
set a 1,
set b 0,
eval,
output;
set a 1,
set b 1,
eval,
output;
```
We feed the test file into the simulator along with the following comparison
file and it will compute whether the chip conforms to our expectations:
```
# Nand.cmp
| a | b | out |
| 0 | 0 | 1 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
```
## Coursework
The task for the first unit was to use Hack to create the set of logic gates and
chips that will later be utilised in the construction of the computer. You are
provided with NAND as a primitive and from this you build the other gates. Once
a working gate has been constructed from NAND you are permitted to use it in the
construction of subsequent gates. For example if you have made an OR gate solely
out of NANDs, you may then use OR along with NAND to create XOR.
Below I have listed the HDL files for each gate along with a simulation of the
circuit implementation.
### Gates
#### NOT
```
CHIP Not {
IN in;
OUT out;
PARTS:
Nand(a=in,b=in,out=out);
}
```
<div style="display:flex;margin-top:1.5rem">
<iframe src="https://circuitverse.org/simulator/embed/n2t-not?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
</div>
#### AND
```
CHIP And {
IN a, b;
OUT out;CHIP DMux {
IN in, sel;
OUT a, b;
PARTS:
Not(in=sel,out=nsel);
And(a=in,b=nsel,out=a);
And(a=in,b=sel,out=b);
}
Not(in=w1,out=out);
}
```
<div style="display:flex;margin-top:1.5rem">
<iframe src="https://circuitverse.org/simulator/embed/n2t-and?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
</div>
#### OR
```
CHIP Or {
IN a, b;
OUT out;
PARTS:
Nand(a=a,b=a,out=w1);
Nand(a=b,b=b,out=w2);
Nand(a=w1,b=w2,out=out);
}
```
<div style="display:flex;margin-top:1.5rem">
<iframe src="https://circuitverse.org/simulator/embed/n2t-or?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
</div And(a=in,b=sel,out=w1);
Or(a=w1,b=sel,out=out);
Not(in=out,out=a);>
#### XOR
```
CHIP Xor {
IN a, b;
OUT out;
PARTS:
And(a=a,b=notb,out=w1);
Not(in=b,out=notb);
And(a=b,b=nota,out=w2);
Not(in=a,out=nota);
Or(a=w1,b=w2,out=out);
}
```
<div style="display:flex;margin-top:1.5rem">
<iframe src="https://circuitverse.org/simulator/embed/n2t-xor?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
</div>
### Chips
As well as the basic logic gates, the first unit introduced additional chips
that are essential for constructing a working computer. These chips represent
more complex states that the logical operators but proceed on the same
functional and modular basis: input values are processed internally to produce
output values and the implementation can utilise previously constructed logic
gates.
#### MUX (Multiplexer)
A multiplexer selects one of several input pins and forwards the selection to a
single output pin. There are three pins: two input bits (`A`, `B`) and a
selection bit (`SEL`). When `SEL` is applied the output bit is toggled between
`A` and `B`. Multiplexers are essential to the construction of large digital
circuits as they implement data selection and switching on the basis of logical
conditions.
```
CHIP Mux {
IN a, b, sel;
OUT out;
PARTS:
Not(in=sel,out=w1);
And(a=w1,b=a,out=w2);
And(a=sel,b=b,out=w3);
Or(a=w2,b=w3,out=out);
}
```
<div style="display:flex;margin-top:1.5rem">
<iframe src="https://circuitverse.org/simulator/embed/mux_n2t?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
</div>
#### DMUX (Demultiplexer)
As the name suggests, a demultiplexer reverses the functionality of a
multiplexer. It receives a single input, and based on the `SEL` value channels
it to either an `A` or `B` output.
```
CHIP DMux {
IN in, sel;
OUT a, b;
PARTS:
Not(in=sel,out=nsel);
And(a=in,b=nsel,out=a);
And(a=in,b=sel,out=b);
}
```
<div style="display:flex;margin-top:1.5rem">
<iframe src="https://circuitverse.org/simulator/embed/dmux_v2_n2t?theme=default&display_title=false&clock_time=true&fullscreen=true&zoom_in_out=true" style="border-width:; border-style: solid; border-color:;" name="myiframe" id="projectPreview" scrolling="no" frameborder="1" marginheight="0px" marginwidth="0px" height="250" width="100%" allowFullScreen></iframe>
</div>
### Multi-bit chips
Multi-bit chips are variants of the chips and gates already produced. The logic
of a multi-bit AND is the same as the logic for a normal AND gate. They differ
only in the number of bits they can receive and output.
In a real computer, passing single 1s and 0s into chips would be inefficient
since very little information can be represented or encoded in a single bit.
When we build the computer we will be passing values with a bit-length of 8-bits
(a byte) as a miniumum (e.g. 10101100) and we need chips that can handle bits of
this length.
For illustration, here is the HDL implementation of an AND-16:
```
CHIP And16 {
IN a[16], b[16];
OUT out[16];
PARTS:
And(a=a[0],b=b[0],out=out[0]);
And(a=a[1],b=b[1],out=out[1]);
And(a=a[2],b=b[2],out=out[2]);
And(a=a[3],b=b[3],out=out[3]);
And(a=a[4],b=b[4],out=out[4]);
And(a=a[5],b=b[5],out=out[5]);
And(a=a[6],b=b[6],out=out[6]);
And(a=a[7],b=b[7],out=out[7]);
And(a=a[8],b=b[8],out=out[8]);
And(a=a[9],b=b[9],out=out[9]);
And(a=a[10],b=b[10],out=out[10]);
And(a=a[11],b=b[11],out=out[11]);
And(a=a[12],b=b[12],out=out[12]);
And(a=a[13],b=b[13],out=out[13]);
And(a=a[14],b=b[14],out=out[14]);
And(a=a[15],b=b[15],out=out[15]);
}
```
Instead of a single-bit AND gate that takes two single-bit inputs and produces a
single-bit output, the 16-bit AND takes two 16-bit inputs and produces a single
16-bit output. Each bit of the output is determined by the AND operation which
is executed on each of the input bits.
Don't be confused by the base-10 numbers: we are still working with binary
values however we use denary digits to individuate each bit in the 16-bit
number. For example if `a = 10101111`, `a[4]` refers to the fourth bit in `a`
counting from the right-hand side (`0`).
In addition to `And16` I created multi-bit variants of OR, NOT, MUX and DMUX.
### Multi-way chips
I also produced _multi-way_ variants of some of the main gates and chips. These
versions accept more than the standard one or two input pins but execute the
same logic. For example instead of a standard 2-pin input AND gate, a 3-pin
input AND gate would take three inputs and produce a `1` output when all three
inputs are `1`.
An example of a multi-way chip that I constructed is OR-8-WAY. This chip outputs
1 when any of its 8 inputs is 1. If all inputs are 0, it outputs 0:
```
CHIP Or8Way {
IN in[8];
OUT out;
PARTS:
Or(a=in[0],b=in[1],out=a);
Or(a=a,b=in[2],out=b);
Or(a=b,b=in[3],out=c);
Or(a=c,b=in[4],out=d);
Or(a=d,b=in[5],out=e);
Or(a=e,b=in[6],out=f);
Or(a=f,b=in[7],out=out);
}
```

View file

@ -0,0 +1,36 @@
---
title: "Frontend for Eòlas"
slug: /neuron-eolas-frontend/
date: 2024-10-24
tags: ["projects", "log", "eolas"]
---
![Diagram of service](./img/eolas-static-diag.png)
I'm now publishing [my zettelkasten](https://github.com/thomasabishop/eolas)
publicly at
[https://thomasabishop.github.io/eolas/](https://thomasabishop.github.io/eolas/).
I wanted to be able to access my notes from different devices and also have them
in a nicer format with syntax highlighting and LaTeX support.
I'm using the [Neuron](https://neuron.zettel.page/) static site generator which
is designed specifically zettelkasten-type projects. It requires that the links
and some other metadata are in a specific format. However, I wanted the
publishing process to be decoupled from my actual notes and not have to make
arbitrary changes to suit Neuron.
So I wrote a simple Python application
([neuron-zk-generator](https://github.com/thomasabishop/neuron-zk-generator))
that copies the source notes into a specific directory and applies all necessary
formatting to this copy. This generator executes locally when I push to the
remote and creates a Neuron directory which is then built (via a GitHub Action)
and deployed via GitHub pages.
![Neuron generator output](./img/running-neuron-generator.png)
<div style="text-align:center">
It applies some basic CSS changes and also generates dynamic content for the
home page, displaying the build ID, publication date, a page count and the most
recently updated files along with an index:
![Screenshot of Neuron static site](./img/cropped-eolas-fe.png)

27
posts/new-job.md Normal file
View file

@ -0,0 +1,27 @@
---
title: "New job"
slug: /new-job/
date: 2024-11-12
tags: ["personal"]
---
After two years and a couple of months I am leaving the BBC for ITV. I enjoyed
my time at the BBC. I was fortunate to have a really supportive and personable
manager who was great to work with and who took a real interest in my
professional development.
![](./img/bbc-leave-gruv-small.png)
I worked with a variety of stakeholders on applications used internally within
the Corporation. I was able to work across the frontend and the backend in a
"full-stack" capacity, enhancing my understanding of AWS serverless, Docker,
GraphQL and other backend technologies.
In my new role I will be working with a similar tech stack also on internal
solutions, however this will be purely backend which better suits my technical
interests.
After the BBC, ITV is the second biggest broadcaster in the UK, so I think there
will be a lot of overlap. It will be interesting to see how the technical
culture differs and I am looking forward to working in a new environment on new
projects.

View file

@ -0,0 +1,127 @@
---
title: "Note-taking routine"
slug: /note-taking-routine/
date: 2024-03-01
tags: ["log", "productivity", "zettelkasten", "personal", "eolas"]
---
![Notes view](./img/full-note-view.png)
I keep my notes in a repository called [Eólas](). Eólas (_awlus_) is an Irish
word meaning knowledge or information, especially knowledge gained by experience
or practice.
I have designed it as a technical knowledge management system or "second-brain"
comprising notes from my autodidactic study of software engineering and computer
science.
It is a [Zettelkasten](https://en.wikipedia.org/wiki/Zettelkasten) work in
progress. It previously had a hierarchical structure made up of topic-based
subdirectories however I have recently converted this into a single flat
directory structure organised by tags. I'm in the process of partitioning longer
notes into smaller informational units.
I use the [zk](https://github.com/zk-org/zk) CLI package to help with indexing
and task automation alongside its [zk-nvim](https://github.com/zk-org/zk-nvim)
Neovim wrapper (my main editor). I occassionally utilise
[Obsidian](https://obsidian.md/) alongside Neovim for when I want to view my
notes as a knowledge graph or read them alongside their rich content (images,
videos etc).
![Viewing backlinks in Telescope](./img/zk-tags.png)
## Commands
On my local machine I have aliased several commands to help me manage the
knowledge base:
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Alias</th>
<th>Command</th>
<th>Output</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>z</code></td>
<td><code>cd $HOME/repos/eolas</code></td>
<td>Access Zettelkasten</td>
</tr>
<tr>
<td><code>zn</code></td>
<td><code>zk new --title ...</code></td>
<td>Create new entry from template</td>
</tr>
<tr>
<td><code>&lt;leader&gt; zk</code></td>
<td><code>:ZkNotes</code></td>
<td>Access Zettelkasten from anywhere within <code>nvim</code></td>
</tr>
<tr>
<td><code>&lt;leader&gt; zi</code></td>
<td><code>:ZkIndex</code></td>
<td>Index Zettelkasten within <code>nvim</code></td>
</tr>
<tr>
<td><code>&lt;leader&gt; zt</code></td>
<td><code>:ZkTags</code></td>
<td>View tags via <a href="https://github.com/nvim-telescope/telescope.nvim">Telescope</a> within <code>nvim</code></td>
</tr>
<tr>
<td><code>&lt;leader&gt; ztt</code></td>
<td><code>:ObsidianTags</code></td>
<td>View tags in a Vim buffer via within <code>nvim</code> using <a
href="https://github.com/epwalsh/obsidian.nvim">obsidian-nvim</a></td>
</tr>
<tr>
<td><code>&lt;leader&gt; zl</code></td>
<td><code>:ZkLinks</code></td>
<td>View links in current entry via Telescope within <code>nvim</code>, using <a
href="https://github.com/epwalsh/obsidian.nvim">obsidian-nvim</a></td>
</tr>
<tr>
<td><code>&lt;leader&gt; zb</code></td>
<td><code>:ZkBacklinks</code></td>
<td>View backlinks to current entry via Telescope within <code>nvim</code>, using <a
href="https://github.com/epwalsh/obsidian.nvim">obsidian-nvim</a></td>
</tr>
</tbody>
</table>
## Frontmatter
When I run the `zn` command this generates a new Zettelkasten entry with the
following frontmatter template:
```yaml
---
id: o8yzcrtv
title: test
tags: []
created: Saturday, February 17, 2024 | 17:44
---
```
## Scripts
The [scripts](https://github.com/thomasabishop/eolas/tree/master/scripts)
directory contains several Bash and Python scripts I use for general
housekeeping, such as formatting image URLs, removing unused assets, and
autosaving.
## Autosave
I use a
[bash script](https://github.com/thomasabishop/eolas/blob/master/scripts/auto_save.sh)
to create autosave functionality via Git. This script runs every 15 minutes via
a `cron` timer. It tidies up the directory (removes unused images, ensures all
file names use underscores rather than spaces and hyphens etc) and commits and
pushes to GitHub.
## Usage
I want to be able to access my notes instantly whatever I am working on so I
typically have them open, alongside my knowledge graph and GPT-4 client in a
secret [Hyprland](https://hyprland.org/) window overlay (pictured above).

70
posts/recent-courses.md Normal file
View file

@ -0,0 +1,70 @@
---
title: "Recent courses"
slug: /recent-courses/
date: 2023-05-03
tags: ["log", "personal"]
---
Below are the recent courses I have completed.
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Course</th>
<th>Provider</th>
<th>Date completed</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/graphql_essential_training_191122.pdf">GraphQL Essential Training</a></td>
<td>LinkedIn Learning</td>
<td>19-11-2022</td>
</tr>
<tr>
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/apollo_associate_graph_developer_251122.pdf">Graph Developer Associate</a></td>
<td>Apollo GraphQL</td>
<td>25-11-2022</td>
</tr>
<tr>
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/learning_aws_for_developers_031222.pdf">Learning AWS for Developers</a></td>
<td>LinkedIn Learning</td>
<td>03-12-2022</td>
</tr>
<tr>
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/mysql_essential_training_110123.pdf">MySQL Essential Training</a></td>
<td>LinkedIn Learning</td>
<td>11-01-2023</td>
</tr>
<tr>
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/nodejs_essential_training_230123.pdf">NodeJS Essential Training</a></td>
<td>LinkedIn Learning</td>
<td>23-01-2023</td>
</tr>
<tr>
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/python_programming_150223.pdf">Python Programming (3 Day)</a></td>
<td>BBC Internal Training</td>
<td>15-02-2023</td>
</tr>
<tr>
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/learning_bash_scripting_160323.pdf">Learning Bash Scripting</a></td>
<td>LinkedIn Learning</td>
<td>16-03-2023</td>
</tr>
<tr>
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/git_intermediate_techniques_060423.pdf">Git Intermediate Techniques</a></td>
<td>LinkedIn Learning</td>
<td>06-04-2023</td>
</tr>
<tr>
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/learning_aws_lambda_210423.pdf">Learning AWS Lambda</a></td>
<td>LinkedIn Learning</td>
<td>21-04-2023</td>
</tr>
<tr>
<td><a href="https://github.com/thomasabishop/certificates/blob/main/certificates/docker_training_260423.pdf">Essential Docker (3 Day)</a></td>
<td>BBC Internal Training</td>
<td>26-04-2023</td>
</tr>
</tbody>
</table>

View file

@ -0,0 +1,694 @@
---
title: "AWS Lambda: recommended articles"
slug: /recommended-articles-integration
date: 2023-07-07
tags: ["log", "project", "aws", "productivity"]
---
I've just added a new feature to the blog: a page that lists links to articles
that I have found interesting. Here I will walk through the development process
as it is a good example of setting up a simple AWS Lambda and will be useful for
future reference.
## Implementation
![](./img/lambda-diagram-new.png)
<div style="margin-top: 1rem"></div>
I use Pocket to manage my reading list. When I save an article that I want to
share to the blog I tag it with `website`. When querying the Pocket API I use
this tag to distinguish articles I wish to share from my other saves. I deploy a
Lambda function (written in TypeScript) that is accessible through an
[AWS API Gateway](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html)
endpoint. This function simply sends a fetch request to the Pocket API and
returns the response via the Gateway endpoint.
The Lambda is just a wrapper around the API request - I am not storing any data
in a database. Using a Lambda means that I can access my Pocket API credentials
securely on the backend and also sidestep the issue of Pocket not allowing CORS
requests from a frontend.
## Backend
I will use the AWS
[Single Application Model](https://aws.amazon.com/serverless/sam/) (SAM) for
development. This will allow me to develop and provision the Lambda locally and
then deploy my specifications as a template to AWS via the terminal. Once
deployed this template will be used by AWS CloudFormation to create a manage the
resources I have specified. This simplifies and programatises much of the work
involved in creating and running a serverless application on AWS, since you
don't have to fiddle too much with different AWS services in the AWS web
console.
### Create Lambda template
I use the SAM CLI to bootstrap a NodeJS Lambda written in TypeScript:
```sh
sam init --runtime nodejs16.x
```
### Update the defaults
This creates a basic _hello world_ template that I will adapt for my project. I
will also change the naming conventions and file structure as I dislike the
defaults.
My directory stucture is as follows:
```
.
├── pocket-api-lambda/
│ └── src/
│ └── query-pocket/
│ ├── index.ts
│ ├── package.json
│ ├── tsconfig.json
│ └── tests/
└── template.yml
```
### Create an IAM role for the Lambda function
Next I will provision for AWS to create a dedicated IAM role for my function.
This is an executive role that will allow my function to access other services
on my behalf. At deploy time, AWS will create the role and assign it an Amazon
Resource Name (ARN). This is will uniquely identify the function's role within
AWS.
Within the `Resources` object of the YAML template I add:
```yml
QueryPocketFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- "sts:AssumeRole"
Policies:
- PolicyName: lambda-execution-policy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource: "*"
```
This gives the function basic execution rights. I also need to add the IAM role
to the `Outputs` object in the template:
```yml
QueryPocketFunctionIamRole:
Description: "IAM Role created for Query Pocket function"
Value: !GetAtt QueryPocketFunctionRole.Arn
```
This is necessary for AWS to create a dedicated ARN for the function when the
application is deployed.
### Storing API credentials in AWS Secret Manager
In order to access the Pocket API you need to send HTTP POST requests to the
endpoint with the following credentials supplied in the body:
```json
{
"consumer_key": "[consumer_key_value_here]",
"access_token": "[access_token_value_here]"
}
```
Rather than hard code these credentials in my Lambda code, I will follow best
practice and store them in
[AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) which the Lambda
will retrieve at runtime. This adds an extra layer of security and encrypts the
credentials.
First I add the two values to Secrets Manager:
![](./img/secrets-manager-aws-console.png)
Then I add a resource permission to the existing `Policies` array for the
`QueryPocketFunctionIamRole` to allow the Lambda to access the secret,
specifying the ARN of the secret just created:
```yml
- PolicyName: secrets-manager-access
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource: arn:aws:secretsmanager:eu-west-2:[ACCOUNT_ID]:secret:[SECRET_REF]
```
> I have put redacted private info by using square brackets: this isn't SAM
> syntax
### Accessing credentials
Now that the function is permitted to access the given secret. I will write a
function that will retrieve the credentials from Secrets Manager, which I can
invoke from the Lambda:
```typescript
const getPocketCredentials = async (): Promise<PocketCredentials> => {
const secretsManager = new AWS.SecretsManager()
const response = await secretsManager
.getSecretValue({ SecretId: process.env.SECRET_ARN as string })
.promise()
const secretValues = JSON.parse(response.SecretString as string)
if (secretValues) {
return {
accessToken: secretValues.POCKET_ACCESS_TOKEN,
consumerKey: secretValues.POCKET_CONSUMER_KEY,
}
} else {
throw new Error("Failed to return Pocket credentials")
}
}
type PocketCredentials = {
accessToken: string
consumerKey: string
}
```
This function invokes the `SecretsManager` module of the `aws-sdk` to retrieve
the secret and then exposes each key for destructuring within the main body of
the Lambda function. In order to do so, it needs access to the ARN for the given
secret which I have set as an environment variable in my `template.yml`:
```yml
...
Resources:
QueryPocketFunction:
...
Environment:
Variables:
SECRET_ARN:
arn:aws:secretsmanager:eu-west-2:[MY_ACCOUNT_ID]:secret:[MY_SECRET_ARN]
```
This is great for production but when I am working locally, I don't want to keep
making requests to Secrets Manager as this is unnecessary and could become
costly. Instead I will utilise environment variables for the different
deployment context and reserve calls to Secrets Manager for when the Lambda is
executing in production. When working locally, I will source the Pocket
credentials from an env file.
First I create this file at `env/local.env.json`:
```json
{
"QueryPocketFunction": {
"NODE_ENV": "development",
"POCKET_CREDENTIALS": "{\"POCKET_ACCESS_TOKEN\": \"[ACCESS_TOKEN_HERE]\",\"POCKET_CONSUMER_KEY\": \"[CONSUMER_KEY_VALUE_HERE]\"}"
}
}
```
This sets the Node runtime environment to `development` and ensures that the
`POCKET_CREDENTIALS` are sourced from this file rather than Secrets Manager. I
will add this file to the `.gitignore` so that it remains local. You'll notice
that even though this is JSON, I pass `POCKET_CREDENTIALS` as a string rather
than a nested object. This is because environment variables are a feature of the
operating system's shell and most OSs treat them as simple strings rather than
complex data structures.
Next I will update the `Environment` field in the `template.yml` so that it sets
the Node runtime to `production` when it is deployed. I will also add an empty
variable for `POCKET_CREDENTIALS` so that it can be set with the Secrets Manager
values in the production context.
```yml
...
Resources:
QueryPocketFunction:
...
Environment:
Variables:
SECRET_ARN:
arn:aws:secretsmanager:eu-west-2:[MY_ACCOUNT_ID]:secret:[MY_SECRET_ARN]
NODE_ENV: production
POCKET_CREDENTIALS: ""
```
Now I need to update the earlier `getPocketCredentials` function to distinguish
between deployment environments and source the credentials from each location
depending on the runtime:
```ts
const getPocketCredentials = async (): Promise<PocketCredentials> => {
if (process.env.NODE_ENV === "production") {
const secretsManager = new AWS.SecretsManager()
const response = await secretsManager
.getSecretValue({ SecretId: process.env.SECRET_ARN as string })
.promise()
const secretValues = JSON.parse(response.SecretString as string)
if (secretValues) {
return {
accessToken: secretValues.POCKET_ACCESS_TOKEN,
consumerKey: secretValues.POCKET_CONSUMER_KEY,
}
} else {
throw new Error("Failed to return Pocket credentials")
}
} else {
const localCredentials = JSON.parse(
process.env.POCKET_CREDENTIALS as string
)
if (localCredentials) {
return {
accessToken: localCredentials.POCKET_ACCESS_TOKEN,
consumerKey: localCredentials.POCKET_CONSUMER_KEY,
}
} else {
throw new Error("Failed to return Pocket credentials")
}
}
}
```
Now when I run `sam build && sam local start-api` it will run the production
version, sourcing the credentials from Secrets Manager. When I run the same
command but specify the local environment variables, it will bypass Secrets
Manager and get the credentials from the JSON file:
```sh
sam build && sam local start-api --env-vars /home/thomas/repos/lambdas/pocket-api-lambda/env/local.env.json
```
This command is a bit unwieldy to paste everytime, so I will write a Makefile
that stores it:
```
.PHONY: clean build
clean:
rm -rf .aws-sam
build:
sam build
start-local:
make build && sam local start-api --env-vars /home/thomas/repos/lambdas/pocket-api-lambda/env/local.env.json
```
Now I can just run `make start-local` to get the local API endpoint up and
running.
### Lambda handler function
Finally I will write the actual handler function:
```ts
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
let response: APIGatewayProxyResult
const endpoint = `https://getpocket.com/v3/get`
try {
const { accessToken, consumerKey } = await getPocketCredentials()
const tag = event.queryStringParameters?.tag
const responseHeaders = {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
}
const requestBody = {
consumer_key: consumerKey,
access_token: accessToken,
state: "all",
tag: tag,
}
const options: RequestInit = {
method: "POST",
headers: {
"Content-Type": "application/json; charset=UTF8",
},
body: JSON.stringify(requestBody),
}
const request: Response = await fetch(endpoint, options)
const requestJson: any = await request.json()
response = {
statusCode: 200,
headers: responseHeaders,
body: JSON.stringify({
data: requestJson,
}),
}
} catch (err: unknown) {
console.error(err)
response = {
statusCode: 500,
header: responseHeaders
body: JSON.stringify({
message: err instanceof Error ? err.message : "some error happened",
}),
}
}
return response
}
```
I'm sending a POST request to the Pocket API endpoint. If successful, I return
the data as the response, else I return an error code. You'll notice I am
retrieving `queryStringParameter` from the `event` object. This means I can pass
in different tags later, in case I want to retrieve different articles.
Locally, I would call the endpoint using the following URL:
`http://127.0.0.1:3000/query-pocket/get-articles-by-tag?tag=website`
### The template file in full
Now the configuration is complete and the Lambda is written let's take a look at
the final `template.yml`:
```yml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
pocket-api-lambda
Globals:
Function:
Timeout: 10
MemorySize: 256
Resources:
QueryPocketFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- "sts:AssumeRole"
Policies:
- PolicyName: lambda-execution-policy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource: "*"
- PolicyName: secrets-manager-access
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource: arn:aws:secretsmanager:eu-west-2:[ACCOUNT_ID]:secret:[SECRET_ID]
QueryPocketFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/query-pocket/
Role: !GetAtt QueryPocketFunctionRole.Arn
Handler: index.handler
Runtime: nodejs16.x
Architectures:
- x86_64
Events:
QueryPocketApi:
Type: Api
Properties:
Path: /query-pocket/get-articles-by-tag
Method: get
Environment:
Variables:
SECRET_ARN: arn:aws:secretsmanager:eu-west-2:[ACCOUNT_ID]:secret:[SECRET_ID]
NODE_ENV: production
POCKET_CREDENTIALS: ""
Metadata:
BuildMethod: esbuild
BuildProperties:
Minify: true
Target: "es2020"
Sourcemap: true
EntryPoints:
- index.ts
Outputs:
QueryPocketApi:
Description:
"API Gateway endpoint URL for Prod stage for Query Pocket function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/query-pocket/get-articles-by-tag"
QueryPocketFunction:
Description: "Query Pocket Lambda Function ARN"
Value: !GetAtt QueryPocketFunction.Arn
QueryPocketFunctionIamRole:
Description: "IAM Role created for Query Pocket function"
Value: !GetAtt QueryPocketFunctionRole.Arn
```
To summarise, and cover the parts of the template I have not already explained:
- I am creating provisions for three entities:
- A Lambda function
- An API Gateway enpoint that will trigger the function
- An IAM role for the Lambda function
- In addition I am setting permissions so that my Lambda function can access a
value stored in Secrets Manager, along with associated environment variables.
- I have a single endpoint: `/query-pocket/get-articles-by-tag`. I have given it
it's own path rather than just using the root `/query-pocket` because I may
add additional endpoints in the future
- My Lambda function is defined in `/query-pocket/index.ts`
Now when I start the server locally and call the endpoint from Postman I can see
the data returned:
![](./img/query-pocket-lambda-response-postman.png)
### Deploying the function
I start by validating my template to ensure that I do not introduce problems at
the outset of the deployment:
```
sam validate
/home/thomas/repos/lambdas/pocket-api-lambda/template.yaml is a valid SAM Template
```
Then I run a build:
```
sam build
```
Next I package the application. CloudFront can only receive one file as an
input. When we package the application we create this single file. The packaging
proces will first archive all of the project artefacts into a zip file and then
upload that to an [AWS S3](https://aws.amazon.com/s3/) bucket. A reference to
this S3 entity is then provided to CloudFormation and used to retrieve the
requisite artefacts.
```
sam package
--template-file template.yaml
--output-template-file pkg.yaml
--region eu-west-2
```
This outputs the following, confirming the creation of the zipped resources:
```
Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-xc8swn6xu4km
A different default S3 bucket can be set in samconfig.toml
Or by specifying --s3-bucket explicitly.
Uploading to 67c241b6f81524ee092adab3ba9998c5 50691677 / 50691677 (100.00%)
Successfully packaged artifacts and wrote output template to file pkg.yaml.
Execute the following command to deploy the packaged template
sam deploy --template-file /home/thomas/repos/lambdas/pocket-api-lambda/pkg.yaml --stack-name <YOUR STACK NAME>
```
Now that we have our packaged application, the last step is to deploy it:
```
sam deploy --guided
```
This presents me with options for the deployment defaults. The most important
one is `Allow SAM CLI IAM role creation`. By affirming this, AWS will create the
`QueryPocketFunctionIamRole` and generate an ARN for it.
This takes a few minutes but the CLI will keep you up to date with the
deployment process. Then, if successful, it will confirm the resources it has
created:
![](./img/cloud-formation-cli-confirmation.png)
If I go to CloudFormation in the AWS I will see that the `pocket-api-lambda`
stack now exists:
![](./img/new-cloud-formation-application.png)
And a new function is added to AWS Lambda with the API Gateway endpoint as the
trigger:
![](./img/newly-created-pocket-lambda.png)
And in API Gateway the API has also been set up:
![](./img/api-gateway-pocket-api.png)
## Frontend
My frontend is built with the GatsbyJS library and so is React-based.
### Environment variables
Early I took to trouble to run both development and production versions of the
API. When I am working locally on my site I want to be querying the local
endpoint rather than the production version since this will accrue fees and also
requires me to deploy after every change.
I will use an environment variable to distinguish the local and production
endpoints. I do this using the [dotenv](https://www.npmjs.com/package/dotenv)
Node package and create the following two files:
```sh
# .env.production
GATSBY_POCKET_AWS_LAMBDA_ENDPOINT=https://[HASH].execute-api.eu-west-2.amazonaws.com/Prod/query-pocket/get-articles-by-tag
```
```sh
# .env.development
GATSBY_POCKET_AWS_LAMBDA_ENDPOINT=http://127.0.0.1:3000/query-pocket/get-articles-by-tag
```
By prepending the variable name with `GATSBY`, Gatsby will know to inject these
values at runtime. This way, and by adding my `.env` files to the `.gitignore`,
I can avoid hardcoding my endpoints and can use a single reference in my React
component.
When working locally I can run `NODE_ENV=development npm run start` to source
the local endpoint variable and `NODE_ENV=production npm run start` to source
the production endpoint variable. In the deployed production context however,
`.env.production` will not be available because it is an ignored file. Thus to
source the variables when `npm run build` is run remotely, it is necessary to
store them as secrets that can be accessed in my CI/CD pipeline. I use a
[GitHub Action](https://systemsobscure.blog/how-I-deploy-this-site/) to build my
site and deploy it to AWS when I push changes, so I add the following line to my
`main.file` build script:
```yml
steps:
- name: Set environment variables
run: |
echo "GATSBY_POCKET_AWS_LAMBDA_ENDPOINT=${{ secrets.GATSBY_POCKET_AWS_LAMBDA_ENDPOINT }}" >> $GITHUB_ENV
echo "GATSBY_METRICS_AWS_LAMBDA_ENDPOINT=${{ secrets.GATSBY_METRICS_AWS_LAMBDA_ENDPOINT }}" >> $GITHUB_ENV
```
### React component
Below is the React component:
```jsx
const ENDPOINT = process.env.GATSBY_POCKET_AWS_LAMBDA_ENDPOINT
const ArticleListing = ({ article }) => {
return (
<tr>
<td>
<a href={article?.resolved_url} target="_blank">
{article?.resolved_title}
</a>
</td>
<td>{formatUnixTimestamp(article?.time_added)}</td>
</tr>
)
}
export default function RecommendedArticlesPage() {
const [loading, setLoading] = useState(false)
const [data, setData] = useState({})
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true)
const response = await axios.get(`${ENDPOINT}?tag=website`)
setData(response?.data?.data?.list)
} catch (error) {
console.error(error)
} finally {
setLoading(false)
}
}
fetchData()
}, [])
const articles = Object.keys(data).map((key) => data[key])
return (
<Main>
<PageHeader headerTitle="Recommended articles" />
<p>
Articles written by others that I have learned from or which present
interesting viewpoints.
</p>
<table className="articles-table">
<thead className={loading ? "loading" : ""}>
<tr>
<th>Title</th>
<th>Date added</th>
</tr>
</thead>
<tbody>
{articles?.map((article) => (
<ArticleListing key={article?.item_id} article={article} />
))}
</tbody>
</table>
</Main>
)
}
```
Here I source my environment variable from the Node runtime context and use it
to query the API via the React `useEffect` hook that runs once on load. Then I
loop through the data to populate the rows of an HTML table.
## Addendum: authentication
I have set up a basic AWS Gateway API with two execution contexts (local and
production) that trigger a Lambda function. In closing I want to note that a
considerable oversight is the lack of authentication for the remote endpoint. In
its current state anyone with access to the production URL can call the API.
In this context it's not that big of a deal since the information is not
sensitive or particularly interesting however if I was following best practice I
would need to provide a way for AWS to authenticate requests. I did attempt this
using an IAM role and an AWS Cognito user pool however I was unable to get this
working despite my best efforts. This is something I will return to in a future
post.

View file

@ -0,0 +1,23 @@
---
title: "Requiem for Gruvbox95"
slug: /requiem-for-gruvbox-95/
date: 2024-07-31
tags: ["projects", "gruvbox", "beige"]
---
![Gruvbox95 screenshot](./img/gruvbox95.png)
Back in April I was working on a full system rice. It was going to be called
_Gruvbox95_ and would basically apply the aesthetics of Windows95/98 to the
Hyprland window manager but with the seductive beige of the Gruvbox light theme.
Alas I was ultimately defeated by box-shadow and my own limitations but I came
across a screenshot when I was clearing out my phone.
![Gruvbox95 detail: menubar](./img/toolbar-three.png)
![Gruvbox95 detail: menubar](./img/toolbar-one.png)
![App launcher](./img/file-launcher.png)
![Pixel art icon intended for the launch button.](./img/tux-icon.png)

View file

@ -0,0 +1,38 @@
---
title: "Bash script: random revision topic"
slug: /revision-bash-script
date: 2022-12-28
tags: ["log", "productivity", "bash"]
---
I keep all my study notes in Markdown format in a
[single repository]('https://github.com/thomasabishop/computer_science').
Because I study a lot of varied topics it can sometimes seem that I am just
writing notes and forgetting about them. This is particularly true of topics
that I don't draw on in my everyday work.
As a corrective I have written a script at the root of my notes repository that
selects a random revision topic from the categories I specify. When I start
studying each morning (I study from 6am-8am before work) I run the script and
spend ten minutes consolidating the topic it selects. Sometimes this will just
mean re-reading the notes but often I will rewrite or add ideas that I have
gleaned in the period since I originally studied the topic, extending my
understanding of the subject matter.
```bash
#!/bin/bash
# Choose source directories...
DIRS_TO_PARSE="../Computer_Architecture ../Electronics_and_Hardware ../Operating_Systems ../Programming_Languages/Shell ../Logic"
# Return array of all files belonging to source dirs...
for ele in $DIRS_TO_PARSE; do
FILE_MATCHES+=( $(find $ele -name "\*.md" -type f) )
done
# Generate a random integer between 0 and the match array length...
RANDOM_FILE_INDEX=$(( $RANDOM % ${#FILE_MATCHES[@]} + 0 ))
# Return file matching that index...
echo "Revise this topic: ${FILE_MATCHES[$RANDOM_FILE_INDEX]}"
```

View file

@ -0,0 +1,27 @@
---
title: "AWS Lambda: save articles"
slug: /save-articles-lambda
date: 2023-09-07
tags: ["log", "project", "productivity", "aws", "python"]
---
I've just finished a new
[AWS Lambda](https://github.com/thomasabishop/lambdas/tree/main/save-articles)
written in Python. It retrieves entries from my saved articles in Pocket, parses
the key metadata and saves it to a Google Sheet.
I've done this because I find the Pocket app to be really uncongenial and
confusing to use. However I've used Pocket for a long time and the alternatives
are worse or have bad APIs.
Now I keep my interactions with Pocket minimal. I use the Pocket browser
extension only to save and tag articles. My lambda function retrieves articles
by tag and saves them to a Google Sheet. This allows me to access the articles
from anywhere without using a database.
Each tag has a corresponding sheet that lists the articles in reverse
chronological order. The lambda executes on a cron timer once a day.
Here are my saved technical articles, for example:
![](./img/saved_tech_articles.png)

View file

@ -0,0 +1,319 @@
---
title: "Self-hosting: initial setup"
slug: /self-hosting-1-initial-setup/
date: 2025-02-23
tags: ["projects", "self-hosting"]
---
With the way the Internet (and the world) is going, the benefits of self-hosting
the services you rely on is becoming increasingly apparent.
I have been using AWS to run a limited set microservices on the Free Tier but
doing anything interesting with an actual server and database becomes expensive
quickly. While I hope to eventually run my own physical server, for now I have
opted to maintain a remote virtual private server. The skills I gain here will
obviously transfer to the physical context.
I intend to run a series of third-party FOSS applications on different
subdomains via Docker containers. I will also be deploying my own software using
Docker.
This post goes over the initial setup and configuration of the server.
## Choosing a provider
Jurisdiction, location and price were the main considerations. I want the server
to be based in a country with good privacy laws but not so far away that latency
would be noticeable. I also don't want to pay that much initially given that I
am just starting out.
![The Hetzner Cloud Console](./img/hetzner-console.png)
I opted for the German company, Hetzner. Being in the EU means their digital
privacy laws are better than the UK and Germany's recent history has created
more of a culture of resistance to surveillance than other EU states.
I chose their CX22 Cloud package which provides the following for just under €4
a month:
- 2 virtual CPUs (Intel)
- 4 GB RAM
- 40 GB SSD
- IPv4 and free IPv6
This is modest but given that it's virtual, I can scale up later as necessary.
![My server](./img/hetzner-server.png)
## Creating a server
Once you've paid you get access to the Hetzner Cloud Console and you just click
"Add server" and specify the location, hostname and OS. I opted for Nuremberg as
it's nearest and decided on Debian as my Linux distro.
I set up a basic firewall using the Hetzner console, allowing incoming traffic
on ports 22 (SSH), 80 (HTTP), and 443 (HTTPS). This will apply at the Network
Layer. In a moment I'll explain how I also created a firewall at the Application
Layer, over SSH.
## Connecting
Once the server was spun up the obvious next step was to connect to it and start
configuring from the inside. To do this I needed SSH access. This was
straightforward, I just added my local machine's public key via the Hetzner
console and then connected with:
```sh
ssh root@<server_ip4_address>
```
## Create non-root user
Once I had access as root the first thing I did was update my packages:
```sh
apt update
apt upgrade
```
Next I created a non-root user with `sudo` privileges. This is the account I
will use for administrating the server which is obviously safer than doing stuff
as root:
```sh
apt install sudo
add user <my_username>
usermod -aG sudo <my_username>
```
I then switched to this user:
```sh
su - <my_username>
```
I'm connecting to the server over SSH but I also want to login as `my_username`
using SSH keys instead of a password as this is less secure.
I set up the necessary SSH directory and permissions for `my_username`:
```sh
mkdir /home/<my_username>/.ssh
chown <my_username>:<my_username> /home/<my_username>/.ssh
chmod 700 /home/<my_username>/.ssh
```
Then I transferred my local machine's public key, that I added to the server via
the Hetzner Console earlier, to the known hosts for `my_username`.
```sh
cp /root/.ssh/authorized_keys /home/<my_username>/.ssh/
chown <my_username>:<my_username> /home/<my_username>/.ssh/authorized_keys
chmod 600 /home/<my_username>/.ssh
```
To confirm, I closed the connection and then connected to the server again but
this time as `my_username`:
```sh
ssh <my_username>@<server_ip4_address>
```
Having confirmed SSH login I installed a lightweight version of vim
([vim-tiny](https://www.baeldung.com/linux/vim-tiny-properties)) to make
text-editing easier.
```sh
sudo apt install vim-tiny
```
## Disable root login and access
An important server-hardening routine is to prevent password logins as root.
To do this I edited the root SSH config:
```sh
sudo vim /etc/sshd_config
```
And added the following:
```
PasswordAuthentication no
```
Then I restarted the SSH daemon:
```
sudo systemctl restart sshd
```
Then when I attempted `ssh root@<server_ip4_address>` I was met with "Permission
denied", as intended.
As I side note, it's actually not possible for me to login as root at all, which
is best practice. I haven't set a root password, so if I attempt to login as
root when logged in as `my_username` (with `su -`), all attempts will fail.
When I need to run processes as root, I will do this exclusively by assuming
`sudo` and to do this, I need to enter the password for `my_username`.
## Firewall
Earlier, I set up a firewall from outside of the server, using the Hetzner
console. I'm now going to do the same thing but from within the server as
`my_username`. It will share the same rules. This means I have a firewall at the
Network Layer (Hetzner) blocking requests before they reach the server and a
firewall operating at the Application Layer for those requests that get to the
server.
This doesn't add much in terms of security, especially as the rules are
identical, but having a peripheral firewall on the Network and a firewall on the
server provides a degree of "defence in depth". It will also potentially reduce
the load on the server if blocked requests are deflected at the Network Layer
before reaching the server.
It would be better to block by IP, and only open the SSH port to requests from
my local network (HTTP/HTTPS is obviously public and open to all). This isn't an
option because my my broadband package doesn't include a static IP. One way
around this would be to deploy another Hetzner server and set it up as a VPN. I
would then restrict port 22 to requests from that server's IP and connect to the
VPN server first. This might be a project further down the line.
To set up the host firewall I used `ufw` (Uncomplicated Firewall):
```sh
sudo apt install ufw
```
This is a wrapper for the venerable `iptables` that simplifies the process.
I applied the same basic rules as earlier:
```sh
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
```
To confirm:
```sh
sudo ufw status
```
```
To Action From
-- ------ ----
22/tcp ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
22/tcp (v6) ALLOW Anywhere (v6)
80/tcp (v6) ALLOW Anywhere (v6)
443/tcp (v6) ALLOW Anywhere (v6)
```
(This automatically applies the rules for IPv6 as well as IPv4.)
## Fail2Ban
As an additional security measure I installed `fail2ban`. This software is
designed to work alongside your firewall to detect and block brute-force login
attempts. You apply it to services running on specific ports at the Application
Layer.
Debian does not come with the `rsyslog` package installed and `fail2ban` works
best with this logging program rather than `systemd` and its `journalctl` logs.
I tried to get it working with `systemd` but kept facing errors I couldn't
resolve, so I just installed `rsyslog` along with `fail2ban`:
```sh
sudo apt install fail2ban
sudo apt install rsyslog
sudo systemctl start rsyslog
sudo systemctl enable rsyslog
sudo systemctl start fail2ban
sudo systemctl enable fail2ban
```
I didn't change any of the `fail2ban` defaults. On Debian it runs on the `sshd`
server automatically and this is the only service I have running currently.
I waited a while and then checked the status:
```sh
sudo fail2ban-client status sshd
```
Which gave me:
```
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 55
| `- File list: /var/log/auth.log
`- Actions
|- Currently banned: 0
|- Total banned: 6
`- Banned IP list:
```
The IP addresses of actors that make repeated failed login attempts are put in a
temporary "jail" by `fail2ban` which means they are blocked for a limited time
period. The data above tells me that there have been 55 total attempted failed
logins and this has resulted in 6 IPs being put in jail! However this is just
normal background noise in serverland and nothing to particularly worry about.
When I take a look in `var/log/auth.log`, I see entries like:
```
Disconnected from authenticating user root 218.92.0.236 port 22272 [preauth]
```
This indicates a malicious actor trying gain access as root but being blocked by
the earlier root protections.
There are also entries like this:
```
Connection closed by invalid user postgres 195.211.190.228 port 47454 [preauth]
```
Which shows a speculative attempt to try and connect to a non-existing
PostgreSQL service.
These will typically be bots trying their luck at scale rather than actual
people.
Alongside the malicious attempts I see my own legimate logins and invocations of
root as `sudo`:
```
pam_unix(sudo:session): session opened for user root(uid=0) by <my_username>(uid=1000)
```
This was enough for one Sunday! In my next post I'll detail how I set up DNS and
SSL for the new server.
## Resources
In researching how to do the above, I created or expanded the following entries
in my Zettelkasten:
- [Firewalls](https://thomasabishop.github.io/eolas/Firewalls)
- [UFW firewall management](https://thomasabishop.github.io/eolas/UFW_firewall_management)
- [Disable non-root SSH access](https://thomasabishop.github.io/eolas/Disable_non-root_ssh_access)
- [IP addresses](https://thomasabishop.github.io/eolas/IP_addresses)
- [Internet fundamentals](https://thomasabishop.github.io/eolas/Internet_fundamentals)
- [The Application Layer of the Internet Protocol](https://thomasabishop.github.io/eolas/Application_Layer_of_Internet_Protocol)

View file

@ -0,0 +1,118 @@
---
title: "Self-hosting: setting up DNS and TLS"
slug: /self-hosting-2-dns-tls/
date: 2025-03-03
tags: ["projects", "self-hosting"]
---
In my [previous post](./self-hosting-1-initial-setup.md) I described how I
purchased my VPS package, initialised a server and applied some basic security
safeguards. In this post I'll record how I set up DNS and the certification
necessary to enable encrypted client-server communication over HTTPS.
## DNS
As it stands, my server exists on the Internet but can only be reached via its
IPv4 address. This is sufficient to connect via SSH but eventually I want to
host HTTP services and for this, I'll need a domain name.
I purchased the domain `systemsobscure.net` from Namecheap. I'm not mad about
using a US company but it is indeed cheap and has a better record when it comes
to privacy than its main competitor, Cloudflare.
Once purchased, I needed to set up the A Records that associate my server's IP
address with the domain name:
![Setting up A Records in Namecheap](./img/dns-records-detail.png)
`@` sets the root domain. `*` is a wildcard that represents all subdomains at
the root level. This means I can dedicate specific services to different
subdomains on the same server, e.g _grafana.systemsobscure.net_,
_wallabag.systemsobscure.net_ etc.
I've done this for both my IPv4 address and IPv6 addresses (pixelated).
The DNS resolution propagated very quickly and now instead of using the IP, I
can SSH with the domain name, e.g:
```sh
ssh my_username@systemsobscure.net
```
## TLS certification
Next I needed to generate a TLS certificate for the domain. Certificates provide
a mechanism for independently verifying domain ownership. This serves as a
safeguard against impersonation and man-in-the-middle attacks. (Imagine if
someone was able to impersonate the web address of your online banking provider;
they could intercept and steal your account details.)
Establishing trust through a certificate is a prerequisite for the initiation of
encrypted communication between client and server. Once the client has validated
the server's certificate, it can use the server's public cryptographic key
(supplied with the certificate) to create a shared cipher that will encrypt the
subsequent HTTP messages.
The process essentially works as follows. A client will request a resource on my
server. The server will offer its TLS certificate which contains its public key.
The client will confirm the certificate is valid and issued by a reputable
Certificate Authority. It will then use the public key to send the server an
encrypted message. This will be decrypted by the server (using its private key)
and used to create the shared key that will encrypt communication between the
two hosts.
All of this happens silently at the Transport Layer - a layer lower in the
network stack that the actual applications running over HTTPS (TLS stands for
_Transport Layer Security_). You only become aware of it when your browser
alerts you to the fact that a website has an invalid certificate or is using
unencrypted HTTP.
Thankfully, this algorithmic complexity is not the immediate concern of the
server administrator! You can use a tool called `certbot` to generate the
certificate and prove your domain ownership in a matter of minutes.
[certbot](https://certbot.eff.org/) is a free and open-source ACME client. ACME
stands for _Automatic Certificate Management Environment_. It generates the
initial certificate and automatically renews it after 90 days.
I installed `certbot`:
```sh
sudo apt install certbot
```
Then generated the certificate:
```sh
sudo certbot certonly --stanalone -d systemsobscure.net
```
This command does quite a lot. It creates a temporary web server on port 80 and
then sends a request to [Let's Encrypt](https://letsencrypt.org/) (the body
responsible for generating the certificates), asking for a "challenge" to prove
domain ownership.
Let's Encrypt will send a token to `certbot` and `certbot` will dutifully place
the token in a file that is served at a URL over port 80. Let's Encrypt will
then make an HTTP request to this URL ( e.g
`http://systemsobscure.net/.well-known/acme-challenge/the-token`) confirming
ownership and conferring the certificate.
`certbot` saves the certificate (and several associated cryptographic tokens) to
the `/etc/letsencrypt/live` directory.
![Let's Encrypt certificates](./img/certbot_confirm.png)
As I currently don't have any services running on HTTP, the certificates won't
actually be used yet, but they are ready to go. In my next post I will finally
create my first self-hosted service which can now be served using my dedicated
domain name over HTTPS!
## Resources
In researching how to do the above, I created or expanded the following entries
in my Zettelkasten:
- [Certificates and Certificate Authorities](https://thomasabishop.github.io/eolas/Certificate_authorities)
- [Let's Encrypt](https://thomasabishop.github.io/eolas/Let's_Encrypt)
- [HTTPS](https://thomasabishop.github.io/eolas/HTTPS)

View file

@ -0,0 +1,479 @@
---
title: "Self-hosting: setting up reverse proxy and first service"
slug: /self-hosting-3-reverse-proxy-grafana/
date: 2025-03-12
tags: ["projects", "self-hosting"]
---
In my [previous post](https://systemsobscure.blog/self-hosting-2-dns-tls/) in
the [series](link_to_tag) I explained how I configured the DNS settings for the
server and set up its TLS certificate. I am now in a position to start hosting
services.
## Architecture
![The basic client-server architecture I will be using with my reverse proxy.](./img/reverse-proxy.png)
There are three core components to my self-hosting architecture:
- a public subdomain
- a reverse proxy
- software running in Docker containers
I will use this structure for the majority of my services. Let me explain each
part.
### Subdomain
This is the easiest bit to understand. A subdomain is just an address on a
larger server that groups together a set of related processes or resources. For
example, on my server, _service-a_ and _service-b_ would be accessible at the
following subdomains:
- `service-a.systemsobscure.net`
- `service-b.systemsobscure.net`
To access a given resource over the public Internet, it needs to be reachable
via a URL - subdomains provide this in a clear, hierarchical manner.
Back when I set up the
[DNS records](https://systemsobscure.blog/self-hosting-2-dns-tls/) for the
server, I created an A Record with the wildcard character (`*`). This means that
I can create multiple subdomains and have them be served off of the main
`systemsobscure` domain.
### Reverse proxy
The subdomain is the public address of the resource or service. Clients will
send HTTP requests to this address and to handle them, I need a way to map the
public subdomain to the resource on the server that the client has requested.
This is the role of the reverse proxy.
A reverse proxy serves as a buffer between the server and the incoming client
requests. It inspects the request and directs it to the port of the relevant
running process.
Reverse proxies have other uses and advantages - they can be used as load
balancers and as a security measure to block certain traffic. However my use
will be mostly administrative - directing incoming requests to the right
services.
### Docker
[Docker](https://www.docker.com/) is fairly complex but I want to go into a bit
of detail because I think it is one of the few genuinely innovative technologies
of the last couple of decades. It's remarkable how simple it is to use and how
quickly you can provision complex resources with just a few lines of
configuration.
Docker exploits a capacity native to the Linux kernel: containerisation.
Containers allow you to isolate running processes from their specific runtime
environment into self-contained virtual runtimes.
All operating system processes ultimately share the same computational
resources: memory, disk-space, processor etc. This means that any one process
can monopolise those resources at the expense of the others. For example, you
might be running software that has hit a bug and as a result it freezes and your
mouse movement slows down.
Prior to the advent of containerisation, server management mostly consisted in
balancing the competing resource needs of different processes. With containers,
you group a set of related processes into an isolated group and assign them a
specific amount of virtual memory, disk-space etc. This grouping is partitioned
from other processes on the native OS and the container is ignorant of the
specific machine it is running on. This means it is confined to its container
and cannot unduly affect other system processes. It can also be activated and
deactivated, as needed.
You can provision software just as selectively as hardware, using specific
runtimes and dependencies as required. This practically eradicates the common
issue of conflicts arising between, say, the version of Python you have on your
local machine, and the version required by a third-party software. Moreover, as
containers are _portable_ - they can, in principle, be shared between machines -
you can share software as a container and be confident that it will run on any
machine that can leverage containerisation.
This is where Docker comes in. It's a particular implementation of container
technology that is designed to simplify and standardise the creation and
exchange of containerised software.
A Docker _image_ is a blueprint for creating a specific container. For example,
you might use a MySQL image to create a database within your application. The
image contains everything necessary to run the application: binaries, libraries,
resources, and additional dependencies.
You can combine several images into a single container. In this scenario, you
might also include an OS image to manage the different components. These are
typically stripped-down versions of common Linux distributions.
Docker images are defined in a declarative file (Dockerfile) that specifies the
software to be used, the directory within the container where it should execute,
and, usually, an initialisation command.
To demonstrate, the Dockerfile below sets up a basic Python application using
the public `python:3.8` image. It transfers source files from a directory on the
local machine into to the container, installs dependencies and then starts the
application:
```
FROM python:3.8
WORKDIR /app
COPY . /app
RUN pip install -r requirements.txt
CMD ["python", "./my_script.py"]
```
Docker maintains a [public registry](https://hub.docker.com/) of images that you
can download and use via the Docker CLI.
Hopefully my intended architecture is starting to become apparent: I will run
software on my server using Docker images. Requests will arrive at a given
subdomain and the reverse proxy will channel them the port where the Docker
container is running.
That's enough background, let's get started...
## Grafana
The first service I am going to host is
[Grafana](https://en.wikipedia.org/wiki/Grafana). This is a good software to
start with because it will allow me to easily access server logs and build
dashboards that display performance and capacity metrics about the server and
the services I'm running.
I am going to host Grafana at the `grafana.systemsobscure.net` subdomain and, as
explained, I will run it as a Docker container.
### Code management
All of my services will live in a single monorepo on GitHub. This simplifies
deployment. I will configure the software in this repository on my local machine
and test it on a local server. Then, when I want to deploy , I'll simply push my
changes to the remote and pull them down to the production server over SSH.
To manage each service I will use a Docker Compose file. This is just a more
elaborate Dockerfile that you use to manage multi-container Docker applications
that require more advanced functionality than a single container can provide.
For example, when you have multiple containers, they may need access to a shared
storage device and shared network in order to communicate. You define all this
in the Docker Compose then use a single command (`docker compose up`) to start
all the processes.
The directory structure of the monorepo is as follows:
```
├── proxy
│   └── nginx
│   ├── conf.d
│   │   └── grafana.conf
│   └── docker-compose.yml
└── services
└── grafana
├── docker-compose.yml
├── prometheus
│   └── prometheus.yml
├── promtail
│   └── promtail-config.yml
└── README.md
```
Each service will have its own subdirectory (for example `grafana/` ) containing
a Docker Compose file that configures the software. Certain images within the
Docker Compose (eg. `prometheus`, `promtail`) may require custom configuration
in addition to the Docker Compose file - this will be handled in a dedicated
config file, e.g. `prometheus/prometheus.yml`.
### Building the Grafana Docker container
I don't want to get too bogged down in the details of how I set Grafana up, as
my objective in this pose is to demonstrate the generic architecture. However a
few parts require clarification.
Grafana itself is a sort of parent to different tracking and logging tools. It
provides an integrated interface for a wide variety of tools and you only
install the ones you wish to use.
Each of these sub-services require their own Docker image in addition to the
Grafana Docker image. I will be using the following:
- Loki
- To display server logs
- Promtail
- For collecting logs on the host server
- Prometheus
- Backend service that presents the Grafana frontend with data and which can
be queried
- Node Exporter
- A service that can be used by Prometheus to gather hardware and OS metrics
about the host environment
In `/services/grafana/docker-compose.yml`, I create an entry for each service
(including Grafana itself) specifying the image I want to use:
```yml
services:
prometheus:
image: prom/prometheus:latest
node-exporter:
image: prom/node-exporter:latest
loki:
image: grafana/loki:latest
promtail:
image: grafana/promtail:latest
grafana:
image: grafana/grafana:latest
container_name: grafana
```
Certain containers are going to require their own Docker volumes. A Docker
volume is a form of persistent virtual storage within the container for storing
application data. This will be necessary for Prometheus, Loki, and Grafana so I
add the following to the Docker Compose:
```yml
volumes:
prometheus_data:
loki_data:
grafana_data:
```
This establishes the volumes, then for each image I specify a `volumes` field
that maps the volume to the requisite directory within the container:
```yml
grafana:
image: grafana/grafana:latestki
container_name: grafana
volumes:
- grafana_data:/var/lib/grafana
```
The Grafana application will look for its data at `/var/lib/grafana` at runtime,
hence the volume is mapped to this location.
In addition to volumes, I need to provision Docker networks. I need a network
because the individual applications will need to be able to communicate with
each other and the `grafana` container - which is the parent of the other
applications - will need to communicate with processes outside of itself.
Namely, the reverse proxy that will connect client requests to the Grafana
service.
The following section of the Compose file specifies two networks for this
purpose: `default` which runs internally within the the container, and `web`
which is external to the container which `grafana` and other separate Docker
containers will be able to hook into:
```yml
networks:
default:
web:
external: true
```
Then in my `grafana` declaration, I specify its membership of these networks:
```yml
grafana:
image: grafana/grafana:latestki
container_name: grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
volumes:
- grafana_data:/var/lib/grafana
networks:
- default
- web
```
You'll notice I also include an environment variable for the password. This will
be injected automatically via the presence of a `.env` file on the server
containing the password.
Additionally I specify port `3000` for Grafana to run on. This will be important
later when I connect it to the reverse proxy.
### Deploying the container
I'm now ready to pull the changes from the server and test out the container in
the live environment.
Once I've SSH'd into the server I install Docker:
```sh
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```
I clone my monorepo on the server and then `cd` into the `/services/grafana`
directory and run the Docker Compose start script:
```sh
docker compose up -d
```
The containers comprising the Docker Compose start fine:
![](./img/docker-compose-up.png)
I also confirm that the two networks associated with Grafana (`web` and
`default`) are running as expected:
![](./img/docker-net-ls.png)
## nginx
So far I have succeeded in getting my `grafana` container up and running on port
3000 but I have no way yet of accessing its frontend. To do this I need to set
up and configure the reverse proxy.
I'll use [nginx](https://nginx.org/en/) for this and I'll also run this as a
Docker service, configured in the `proxy/` directory of the monorepo that I
demonstrated earlier.
As with Grafana, everything necessary is detailed in the Docker Compose:
```yml
# proxy/nginx/docker-compose.yml
services:
nginx:
image: nginx:latest
ports:
- "80:80"
- "443:443"
volumes:
- ./conf.d:/etc/nginx/conf.d
- /etc/letsencrypt:/etc/letsencrypt:ro
restart: unless-stopped
networks:
- web
networks:
web:
external: true
```
Key points to note:
- `nginx` will have access to the shared `web` Docker network that we saw
earlier when configuring Grafana. This will enable it to communicate with
other Docker services.
- I pass through the actual location of my TLS certificates on the server
(`/etc/letsencrypt`) to the nginx container, specifying that they are read
only. This is a bit different to Grafana where we were mapping virtual Docker
volumes to locations within the container.
- I map both the server's HTTP (`80`) and HTTPS (`443`) ports through to the
equivalent ports in the `nginx` container.
For each Docker service that `nginx` will proxy, I need to provide a
configuration file within the `nginx/conf.d` directory. (These too are copied
into the container.)
The core process is quite simple. You specify:
- The port you want `nginx` to listen to
- The specific subdomain being requested on that port
- The location of the running server process to which you want to direct the
incoming request
To demonstrate with the Grafana config file:
```
# proxy/nginx/conf.d/grafana.conf
server {
listen 443 ssl;
server_name grafana.systemsobscure.net;
location / {
proxy_pass http://grafana:3000;
}
}
```
This is the basic idea. In fact it's a bit more complicated because we need to
enforce HTTPS and make it such that `grafana.systemsobscure.net` is only served
over HTTPS.
To do this I specify the location of my TLS certificate and set the necessary
response header:
```
# proxy/nginx/conf.d/grafana.conf
server {
listen 443 ssl;
server_name grafana.systemsobscure.net;
ssl_certificate /etc/letsencrypt/live/systemsobscure.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/systemsobscure.net/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
...
}
```
This is fine so long as people always use `https://grafana.systemsobscure.net`
but of course they won't always do this, so we need to redirect any requests to
the HTTP port (80) to the HTTPS service. This is achieved with a simple 301
redirect in the same file:
```
server {
listen 80;
server_name grafana.systemsobscure.net;
location / {
return 301 https://$host$request_uri;
}
}
```
Now any requests to `grafana.systemsobscure.net` will automatically be served at
`https://grafana.systemsobscure.net`.
The final step is to start the `nginx` container:
```sh
cd proxy/nginx
docker compose up -d
```
## Results
With the Grafana container running and the reverse-proxy configured we can
finally navigate to `https://grafana.systemsobscure.net` and check out the live
service!
![](./img/grafana-login-screen.png)
Once I've logged in, I use a Grafana template code to create a default template
that outputs the system metrics provided by the Node Exporter service:
![](./img/grafana-server-metrics-dashboard.png)
I can also go to Loki and look at the server connection logs and track the
activity of the `fail2ban` software I installed in the last post:
![](./img/grafana-varlogs-loki.png)
Later, when I have additional services, I'll add additional dashboards and Loki
queries so I can analyse everything that is going on.
## Resources
In researching how to do the above, I created or expanded the following entries
in my Zettelkasten:
- [Proxies](https://thomasabishop.github.io/eolas/Proxies)
- [Containerization](https://thomasabishop.github.io/eolas/Containerization)
- [Docker images](https://thomasabishop.github.io/eolas/Docker_images)
- [Creating a Docker image](https://thomasabishop.github.io/eolas/Creating_a_Docker_image)
- [Docker containers](https://thomasabishop.github.io/eolas/Docker_containers)
- [Docker storage](https://thomasabishop.github.io/eolas/Docker_storage)
- [Docker Compose and Dockerfile difference](https://thomasabishop.github.io/eolas/Docker_compose_and_Dockerfile_difference)

View file

@ -0,0 +1,114 @@
---
title: "Using Slack as a desktop notification center"
slug: /slack-notification-center/
date: 2024-04-10
tags: ["log", "project", "productivity", "javascript"]
---
My current desktop environment is very bare-bones and consists basically of a
window manager and launcher. One difficulty of this is having to manage
notifications manually.
Triggering notifications is fine, I use
[swaync](https://github.com/ErikReider/SwayNotificationCenter). The problem is
logging them in one place so I can see at a glance what has been going on over a
given time period. There are solutions for the Wayland compositor that I use but
the list of notifications won't persist beyond the current session. They are
also quite ugly with limited options for style customisation.
When I talk about notifcations, I mainly mean alerts triggered by the automated
scripts I have running on my system: backups, auto-commits to my Zettelkasten,
time-tracking etc. I don't care about email notifications, Spotify updates etc
and suppress these.
I thought a good way to solve this would be to forgo a desktop-specific
notification center and instead use Slack. Each notification type has a
dedicated channel which I can post to via a specific webhook URL. On the
free-plan the data will persist for up to three months which is more than
sufficient for my purposes.
This solution has the added benefit that I can access my notification center
from other devices. I can also display notifications from more than one machine
and everything will be syndicated in one place. Additionally, I can leverage
Slack's extensions like the GitHub bot. Finally, using Slack's block
architecture means that I have lots of options when it comes to outputting the
notification contents.
The image below shows the notification center with the channel for this blog
displayed:
![](./img/slack-notification-center.png)
(The channel is called "Ripley" because I am an Alien fan! "processes" is the
parent app to which the different webhooks belong)
I invoke the NodeJS script below when I want to set up a notification:
```js
#!/usr/bin/env node
const process = require("process")
const { exec } = require("child_process")
const axios = require("axios")
const notificationSound =
"mpv /home/thomas/dotfiles/gruvbox-95/sounds/st-notification.mp3"
const slackNotifier = async (channel, { message = "", block = null } = {}) => {
try {
const webhooks = {
test: process.env.SLACK_WEBHOOK_TEST,
backups: process.env.SLACK_WEBHOOK_BACKUPS,
eolas: process.env.SLACK_WEBHOOK_EOLAS,
systems_obscure: process.env.SLACK_WEBHOOK_SYSTEMS_OBSCURE,
time_tracking: process.env.SLACK_WEBHOOK_TIME_TRACKING,
}
const webhookUrl = webhooks[channel]
let payload
if (message) {
payload = { text: message, channel: channel }
} else if (block) {
payload = { blocks: block, channel: channel }
} else {
throw new Error("Either a message or a block must be provided.")
}
const response = await axios.post(webhookUrl, payload)
if (response.status === 200) {
exec(notificationSound)
console.log(`Message successfully sent to ${channel}`)
} else {
console.error(
`Slack API returned non-200 response: ${response.status}`,
response.data
)
}
} catch (error) {
console.error(`Failed to send message to ${channel}:`, error)
}
}
if (require.main === module) {
const [, , channel, message] = process.argv
slackNotifier(channel, { message }).catch(console.error)
}
module.exports = { slackNotifier }
```
An example of it being invoked (from within a script that manages my hourly
backups):
```sh
./slack-notifier/src/index.js 'backups' '💾 Error: backup drive not mounted.'
```
One drawback is that there is sometimes latency between the execution of the
script which contains the notificaton trigger and the appearance of the
notification in Slack. This is because the former happens instantly on my
machine and the latter depends on the network. I can live with this though.

View file

@ -0,0 +1,103 @@
---
title: Some nice TypeScript
slug: /some-nice-typescript/
date: 2023-09-26
tags: ["log", "typescript"]
---
I wanted to share a snippet of a TypeScript function I wrote recently. Although
it's not a particularly interesting scenario, I like it because it is simple and
elegant, if I say so myself.
At the moment I am working on an AWS Lambda that queries the API of the Toggl
time-tracking service to retrieve a list of time entries for a given date range.
The API returns an array of time entries with the following structure:
```json
{
"id": 3136598124,
"workspace_id": 2360906,
"project_id": 193325937,
"task_id": null,
"billable": false,
"start": "2023-09-21T20:10:02+00:00",
"stop": "2023-09-21T20:39:54Z",
"duration": 1792,
"description": "pytest blog post",
"tags": [],
"tag_ids": [],
"duronly": true,
"at": "2023-09-21T20:39:54+00:00",
"server_deleted_at": null,
"user_id": 3700888,
"uid": 3700888,
"wid": 2360906,
"pid": 193325937
}
```
Notice that the `project_id` is a number rather than the human-readable string
that appears in the Toggl UI. When I return the data to the frontend I want to
have the name of the project rather than this number. So I wrote the following
transformer:
```ts
interface IProject {
id: number
name: string
[key: string]: unknown
}
type TProjectMap = Record<IProject["id"], IProject["name"]>
const getProjects = async (): Promise<TProjectMap> => {
const workspace = process.env.TOGGL_WORKSPACE_ID
const togglClient = new TogglClient()
const projects: IProject[] = await togglClient.get(
`workspaces/${workspace}/projects`
)
return parseProjects(projects)
}
const parseProjects = (projects: IProject[]): TProjectMap => {
return projects.reduce((projectMap: TProjectMap, project: IProject) => {
projectMap[project.id] = project.name
return projectMap
}, {})
}
```
`getProjects` obviously calls the Toggl API to retrieve the list of projects for
the workspace. The `parseProjects` function takes the array of projects and
returns an object with the project IDs as keys and the project names as values.
In my main controller function, I will then be able to transform the project IDs
like so:
```ts
const projects = await getProjects()
const projectName = projects[193325937]
console.log(projectName)
// "Practical study"
```
What I like:
- The time-entry returns several properties that I am not interested in - I only
want `id` and `name`. I could manually type the other properties to ensure
full type-safety but life is short and I can't be bothered. One get-out would
be to use `any` for the other properties but this is obviously self-defeating
in TS. Instead I use `unknown` and then type-assert the return value of
`parseProjects` to `TProjectMap` which is a record of `number` keys and
`string` values. This means that if I try to access a property of the
`projectMap` object that is not a number, I will get a type error with the
added benefit that I make it transparent to the reader that I am solely
interested in the `id` and `name` keys.
- Rather than using a `forEach` loop or a `map` function combined with a
`filter`, I make good use of `reduce`, _reduce_ being a very semantic account
of what I am seeking to do.
More generally, I feel like I have purposefully harnessed the explanatory value
of the type system to make the code more readable and self-documenting. This is
a good example of where TypeScript can reduce the need for boilerplate and
comments, in addition to the obvious benefits of type-safety.

View file

@ -0,0 +1,61 @@
---
title: "Suppressing logs and errors in Jest"
slug: /suppressing-logs-errors-jest/
date: 2024-09-10
tags: ["javascript", "unit-testing"]
---
It annoys me when I am working on a project and a previous developer has left
logs and/or thrown errors in their unit tests. To be clear: I mean when the
developer is testing that an error is thrown in the right circumstances, not a
test failure arising from regression. The former makes the latter harder to
detect by polluting the output.
This can be so easily prevented.
Take the following function:
```js
function sillyFunction(int) {
console.info(`Now handling ${int}`)
if (int > 2) throw new Error(`Error: int ${int} is greater than two`)
else return
}
```
To avoid pointlessly logging to the console and to confirm the error without
actually throwing it:
```js
import { jest } from "@jest/globals"
describe("sillyFunction", () => {
beforeEach(() => {
jest.spyOn(console, "info").mockImplementation(() => {})
})
afterEach(() => {
console.info.mockRestore()
})
it("throws error if `int` is less than 2", () => {
expect(() => {
sillyFunction(3)
}).toThrow("Error: int 3 is greater than two")
})
})
```
The `spyOn` method silences the output of `console.error` by returning nothing.
If we wish, we can still confirm that the log is acting as expected during the
runtime of the test by again using a spy:
```js
const consoleInfoSpy = jest.spyOn(console, "info")
expect(consoleInfoSpy).toHaveBeenCalledWith("Now handling x")
```
The `toThrow` method catches the error before it hits the console and allows us
to interrogate it. If it was an asynchronous function under test, we would need
to use the matcher `rejects.toThrow`. This waits for the promise to resolve
before checking if it has been rejected.

View file

@ -0,0 +1,44 @@
---
title: "And the world knew him not (lossless compression)"
slug: /the-world-knew-him-not/
date: 2023-02-16
tags: ["random", "reading"]
---
Last night I was reading 'Three Versions of Judas' from Juan Luis Borges'
masterpiece _Fictions_ (1944). The structure is typical Borges: a cool precis of
a series of fabricated texts, replete with tokens of mock facticity (publication
dates, references to other commentaries, footnotes on errata and so forth).
The story describes the work of an obscure theologian who developed a
revisionist account of the role of Judas in the Passion, broadly in keeping with
the (real) Gnostic
[Gospel of Judas](https://en.wikipedia.org/wiki/Gospel_of_Judas). Far from being
the perennial embodiment of self-interest and betrayal, Judas is reimagined as
an agent of the Holy Spirit. He becomes the essential catalyst in the
fullfilment of Christ's prophesy and thus the redemption of humanity.
The argument is compelling and centers on why it would be necessary to _betray_
Jesus in the first place. His activities were well known to both the Jewish and
Roman authorities. If the same Gospels that cast Judas as the betrayer are to be
believed, Jesus was constantly performing miracles, challenging social and
religious hierarchies and being a major-league nuisance. They could've come for
him at any point.
According to the scholar, Judas was the most significant disciple because he had
a presentiment of the Crucifixion and its cosmic significance, and was compelled
to "betray" Christ and shoulder eternal infamy out of this knowledge. His
suicide by hanging was a pale mirroring of Christ's own death. Hence the kiss
(an act of love) and discarding the silver at the temple (a repudiation of a
simplistic reading of the betrayal). Hence also, Jesus's to Judas at the Last
Supper: "what you are going to do, do quickly".
Anyway, this elaborate preamble has been merely to draw attention to a passage
in John that Borges quotes. Maybe it is just that I am a lapsed Catholic but I
think it is very beautiful and profound: a lossless compression of the entirety
of Christianity to a single sentence:
> He was in the world, and the world was made by him, and the world knew him
> not.
&mdash; John 1: 10, King James Version

View file

@ -0,0 +1,88 @@
---
title: "Two strategies for handling exceptions in Python"
slug: /two-strategies-for-handling-exceptions-in-python/
date: 2023-10-03
tags: ["learning", "python"]
---
The following function illustrates two strategies for handling exceptions in
Python:
```py
def parse_articles(articles: Dict[str, Any]) -> List[List]:
if not articles:
raise ValueError("No articles to parse")
articles_list = []
for article in articles.values():
try:
time_added, given_title, resolved_url = (
article["time_added"],
article["given_title"],
article["resolved_url"],
)
except KeyError as e:
logging.warning(
f"Article missing {e} property. Skipping article: {given_title}."
)
continue
articles_list.append([time_added, given_title, resolved_url])
return articles_list
```
The function is a basic transformer: it receives a dictionary and loops through
a subset of its properties which are also dictionaries, returning selected keys
from each as a multidimensional array.
The first exception handler checks whether the `articles` dictionary is
populated or `None`. If either are the case, the function exits and returns a
`ValueError`. This approach _propagates_ the error and is deliberately and
transparently obstructive: the function exits before it can execute the main
code. If this exception is not adequately handled by the caller, it will cause a
runtime error.
The second instance of exception handling occurs in the loop. If any of the
three specified properties (`time_added`, `given_title`, `resolved_url`) are
absent from the `articles` dictionary, a `KeyError` exception will be thrown.
The outcome is quite different from the first exception however. The function
will not exit immediately, it simply won't append this set of properties to the
list, and will move on to the next iteration of the loop.
This behaviour, known as _graceful degradation_ is afforded by the _try, except_
scaffolding. It literally allows us to `try` an execution before an exception
can be raised. And if an exception is raised, we can augment the code with a
`finally` block to return something regardless of the exception, such as an
empty list.
In the scenario above, `finally` isn't used since the `continue` ensures that
the execution proceeds to the next iteration of the loop. In addition, even if
none of the `articles` contain the requisite keys, an empty list will be still
be returned.
Both strategies, propagation and graceful degradation, have their merits.
Propagation is useful because it identifies the error immediately and blocks the
code execution, meaning that failures do not occur silently or go unchecked,
forcing the caller to implement a handling routine. This is particulaly useful
for genuine exceptions - cases that _should not_ occur in regular execution. If
they occur, they are so severe that they should be addressed immediately, not
deferred. One such scenario might be a memory-intensive script or lambda running
in a severless cloud provider: you would not want it to waste compute time on a
process that is not going to return the expected value.
The obvious benefit of graceful degradation is that it doesn't halt the program
straight away. This is most suitable for cases where the exception in question
is fairly common. In the context of the code example this would be not returning
articles that lack the requisite keys. Providing most of the articles are
parsable, the fact that some are skipped is unlikely to be a terminal fault in
the application.
Finally, the two methods differ in the amount of work required of the developer.
Propagation requires explicit error handling from the function or method that
receives the error. In contrast, graceful degradation allows for silent
failure - the error is not immediately apparent and may only be detected in logs
at a later stage.

View file

@ -0,0 +1,197 @@
---
title: "Waybar customisation"
slug: /waybar-customisation/
date: 2024-03-03
tags: ["log", "productivity", "python", "linux"]
---
On my local machine I am currently replacing my desktop environment (Gnome) with
a tile-based window manager ([Hyprland](https://github.com/hyprwm/Hyprland)).
I'm trying to strip out the bloat and have a lightweight and highly customised
Arch workhorse.
A key component is a useful status bar that displays metrics and provides a
means of quickly executing common tasks. Previously, when I was using X, I opted
for Polybar but now that I am on Wayland I'm using
[waybar](https://github.com/Alexays/Waybar). So far, I've added two custom
modules: one for displaying code metrics and another for managing my
time-tracking.
![](./img/waybar-full.png)
<div style="text-align:center;">
<i >Full status bar. Click to enlarge.</i>
</div>
## Code metrics module
I use WakaTime to collate metrics on my coding activity (time coding,
programming languages, projects etc.) I already have a dashboard on this site
that displays some of this data but I thought it would be useful to see my stats
from the status bar as I am working.
Below is an image of the module. The permanent display outputs the coding
duration for the current day. When you hover you see a summary of the main
languages used and the main projects I've been working on.
![](./img/wakatime-waybar.jpg)
It's a straightforward Python script that queries the WakaTime API and parses
the data and forwards it to the module:
```py
#! /usr/local/bin/python3
import requests
import os
import json
import textwrap
WAKATIME_API_KEY = os.getenv("WAKATIME_API_KEY")
WAKATIME_ENDPOINT = "https://wakatime.com/api/v1/users/current/status_bar/today"
def get_data(url):
response = requests.get(url)
if response.status_code == 200:
return response.json()
else:
raise Exception(
f"Failed to fetch data from API. Status code: {response.status_code}"
)
def generate_tooltip(time, languages, projects):
return textwrap.dedent(
f"""\
Time coding: {time}
Languages: {languages}
Projects: {projects}"""
)
def format_metric(metrics):
return ", ".join(
[f'{metric["name"]} ({metric["percent"]}%)' for metric in metrics[:3]]
)
def main():
output = {}
try:
data = get_data(WAKATIME_ENDPOINT + "?api_key=" + WAKATIME_API_KEY)
digital_time = data["data"]["grand_total"]["digital"]
human_time = data["data"]["grand_total"]["text"]
langs = data["data"]["languages"]
projects = data["data"]["projects"]
tooltip = generate_tooltip(
human_time, format_metric(langs), format_metric(projects)
)
output["text"] = digital_time
output["tooltip"] = tooltip
except Exception as e:
output["text"] = "Error"
print(json.dumps(output))
if __name__ == "__main__":
main()
```
Here's the module declaration in the Waybar config:
```
"custom/wakatime": {
"exec": "source $HOME/dotfiles/.env && python3 $HOME/.config/waybar/resources/custom_modules/wakatime_waybar.py",
"format": "󰅱 {}",
"return-type": "json",
"interval": 600
},
```
## Time-tracking module
I've recently started using [time warrior](https://timewarrior.net/) to track my
extra-curricular coding and study. This is a command-line time-tracker so it
integrates really well with Waybar.
The module highlights green when there is an active timer. This helps to remind
me to stop the timer! When you click the current timer stops and when you
right-click it resumes. This saves me going into the terminal when I stopping
and starting an ongoing piece of work.
![](./img/timer-module.png)
<div style="text-align:center;">
<i >Time warrior module in its active state.</i>
</div>
Again I've used Python for the scripting:
```py
#! /usr/local/bin/python3
import subprocess
import json
def invoke_shell(proc):
try:
result = subprocess.run(
proc,
shell=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
return e.stderr.strip()
def timer_active() -> bool:
status = invoke_shell("timew get dom.active")
if status == "1":
return True
else:
return False
def generate_tooltip():
tooltip = invoke_shell("timew summary :week")
return tooltip
def main():
output = {}
try:
if timer_active():
output["text"] = "󱫐 Timer"
output["class"] = "active"
else:
output["text"] = "󱫦 Timer"
output["class"] = "inactive"
except Exception as e:
output["text"] = "Error"
print(json.dumps(output))
if __name__ == "__main__":
main()
```
And here is the module declaration in the Waybar config:
```
"custom/timewarrior": {
"exec": "python3 $HOME/.config/waybar/resources/custom_modules/time_warrior_waybar.py",
"format": "{}",
"on-click": "timew stop && notify-send 'Time Warrior' 'Timer stopped'",
"on-click-right": "timew continue && notify-send 'Time Warrior' 'Timer resumed'",
"return-type": "json",
"interval": 5
},
```

BIN
public/blog-screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View file

@ -0,0 +1,71 @@
import fs from "fs"
import matter from "gray-matter"
import { marked } from "marked"
import { createHighlighter } from "shiki"
import { transformerColorizedBrackets } from "@shikijs/colorized-brackets"
import { processBlogImages } from "./process-blog-imgs.js"
import { transformImagePaths } from "./transform-img-paths.js"
const renderer = {
image({ href, text }) {
return `<figure><img src=${href} /><figcaption>${text}</figcaption></figure>`
},
}
const highlighter = await createHighlighter({
themes: ["github-dark-dimmed"],
langs: [
"javascript",
"typescript",
"jsx",
"tsx",
"html",
"css",
"json",
"markdown",
"bash",
"python",
"yaml",
],
})
// Copy blog images to /public and compress
processBlogImages()
const files = fs.readdirSync("posts").filter((x) => x.endsWith(".md"))
const posts = files.map((file) => {
const raw = fs.readFileSync(`posts/${file}`, "utf8")
const { data, content: markdown } = matter(raw)
// Convert img urls to source from /public whilst markdown
const transformedMarkdown = transformImagePaths(markdown)
// Appy syntax highlighting
let html = transformedMarkdown.replace(
/```(\w+)?\n([\s\S]*?)```/g,
(match, lang, code) => {
return highlighter.codeToHtml(code.trim(), {
lang: lang || "text",
theme: "github-dark-dimmed",
transformers: [transformerColorizedBrackets()],
})
}
)
// Convert img tags to figure tags
marked.use({ renderer })
console.info(`Processing ${file}...`)
return {
slug: file.replace(".md", ""),
title: data.title,
date: data.date,
tags: data.tags,
html: marked(html),
}
})
fs.writeFileSync("./public/post-index.json", JSON.stringify(posts, null, 2))
console.info(`✅ Generated ${posts.length} posts.`)

View file

@ -0,0 +1,52 @@
import fs from "fs"
import path from "path"
import sharp from "sharp"
const processBlogImages = async () => {
const srcDir = "posts/img"
const destDir = "public/posts/img"
if (!fs.existsSync(srcDir)) {
console.error("⚠️ No posts/img directory found")
return
}
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true })
}
// Copy all images
const files = fs.readdirSync(srcDir)
const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"]
// Use for...of loop instead of forEach to work with async/await
for (const file of files) {
const ext = path.extname(file).toLowerCase()
if (imageExtensions.includes(ext)) {
const inputPath = path.join(srcDir, file) // Define inputPath and outputPath
const outputPath = path.join(destDir, file)
if (ext === ".svg") {
// SVGs just get copied
fs.copyFileSync(inputPath, outputPath)
console.info(`📸 Copied ${file}`)
} else {
// Process other images
await sharp(inputPath)
.resize(1200, 800, {
fit: "inside",
withoutEnlargement: true,
})
.jpeg({ quality: 85, progressive: true })
.png({ compressionLevel: 8 })
.webp({ quality: 85 })
.toFile(outputPath)
console.info(`🖼️ Processed ${file}`)
}
}
}
console.log(`✅ Processed images from ${srcDir}`)
}
export { processBlogImages }

View file

@ -0,0 +1,9 @@
const transformImagePaths = (markdown) => {
// Transform ./img/image-name to /posts/img/image-name
return markdown.replace(
/!\[([^\]]*)\]\(\.\/img\/([^)]+)\)/g,
"![$1](/posts/img/$2)"
)
}
export { transformImagePaths }

105
src/components/Header.tsx Normal file
View file

@ -0,0 +1,105 @@
// @ts-nocheck
import { Button } from "@/components/ui/button"
import { useTheme } from "@/context/ThemeProvider"
import { MoonStar } from "lucide-react"
import { Link } from "react-router"
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
NavigationMenuViewport,
} from "@/components/ui/navigation-menu"
import { Toggle } from "@/components/ui/toggle"
const Menu = () => {
return (
<NavigationMenu>
<NavigationMenuList>
{/* Desktop menu - hidden on mobile, visible on md+ */}
<div className="hidden md:flex md:space-x-1">
<NavigationMenuItem>
<NavigationMenuLink
asChild
className={navigationMenuTriggerStyle()}
>
<Link to="/posts/page/1">Posts</Link>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink
asChild
className={navigationMenuTriggerStyle()}
>
<Link to="/about">About</Link>
</NavigationMenuLink>
</NavigationMenuItem>
</div>
{/* Mobile dropdown - visible only on small screens */}
<NavigationMenuItem className="md:hidden">
<NavigationMenuTrigger>Menu</NavigationMenuTrigger>
<NavigationMenuContent>
<NavigationMenuLink asChild>
<Link href="/docs" className="block px-2 py-1.5">
Posts
</Link>
</NavigationMenuLink>
<NavigationMenuLink asChild>
<Link href="/docs" className="block px-2 py-1.5">
About
</Link>
</NavigationMenuLink>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
<NavigationMenuViewport />
</NavigationMenu>
)
}
const Header = () => {
const { theme, setTheme } = useTheme()
return (
<header className="w-full h-12 flex items-center justify-center border-b fixed top-0 z-20 bg-background">
<div className="w-full px-2 md:px-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<Button variant="ghost" asChild>
<Link to="/">
<span className="text-lg tracking-normal font-semibold">
Systems Obscure
</span>
</Link>
</Button>
</div>
<div className="flex items-center justify-between gap-2">
<Menu />
<Toggle
pressed={theme === "dark"}
onPressedChange={() =>
setTheme(theme === "dark" ? "light" : "dark")
}
>
<MoonStar />
<span className="hidden sm:block">Dark theme</span>
</Toggle>
{/*
<div className="hidden md:block">
<Button variant="ghost">
<GitMerge /> Forgejo
</Button>
</div>
*/}
</div>
</div>
</header>
)
}
export { Header }

View file

@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View file

@ -0,0 +1,59 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View file

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View file

@ -0,0 +1,168 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function NavigationMenu({
className,
children,
viewport = true,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean
}) {
return (
<NavigationMenuPrimitive.Root
data-slot="navigation-menu"
data-viewport={viewport}
className={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
)
}
function NavigationMenuList({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return (
<NavigationMenuPrimitive.List
data-slot="navigation-menu-list"
className={cn(
"group flex flex-1 list-none items-center justify-center gap-1",
className
)}
{...props}
/>
)
}
function NavigationMenuItem({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return (
<NavigationMenuPrimitive.Item
data-slot="navigation-menu-item"
className={cn("relative", className)}
{...props}
/>
)
}
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
)
function NavigationMenuTrigger({
className,
children,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return (
<NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger"
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDownIcon
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
)
}
function NavigationMenuContent({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return (
<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
className
)}
{...props}
/>
)
}
function NavigationMenuViewport({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return (
<div
className={cn(
"absolute top-full left-0 isolate z-50 flex justify-center"
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
{...props}
/>
</div>
)
}
function NavigationMenuLink({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return (
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function NavigationMenuIndicator({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
return (
<NavigationMenuPrimitive.Indicator
data-slot="navigation-menu-indicator"
className={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className
)}
{...props}
>
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuPrimitive.Indicator>
)
}
export {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
}

View file

@ -0,0 +1,127 @@
import * as React from "react"
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
)
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />
}
type PaginationLinkProps = {
isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
}
function PaginationPrevious({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
)
}
function PaginationNext({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
)
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
)
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}

View file

@ -0,0 +1,45 @@
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Toggle({
className,
variant,
size,
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Toggle, toggleVariants }

View file

@ -0,0 +1,55 @@
// @ts-nocheck
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Link } from "react-router"
import { convertDate } from "@/utils/convertDate"
const PostListing = ({ posts, title, showAllButton }) => {
return (
<>
<div className="mb-5 ">
<h2 className="scroll-m-20 text-2xl font-semibold lg:text-2xl border-b pb-3">
{title}
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-1 gap-3">
{posts.map((post) => (
<Link
to={`/posts/${post.slug}`}
key={post.slug}
className="block no-underline"
>
<Card
key={post.slug}
className="flex flex-col h-full hover:bg-primary/5 py-4 px-0"
>
<CardHeader className="">
<CardTitle className="leading-snug font-semibold ">
{post.title}
</CardTitle>
<CardDescription className="text-sm text-muted-foreground">
<div className="flex justify-between gap-2">
<span className="text-sm">{convertDate(post.date)}</span>
</div>
</CardDescription>
</CardHeader>
</Card>
</Link>
))}
</div>
{showAllButton && (
<Button asChild variant="secondary" className="w-full mt-4">
<Link to="/posts/page/1">View all</Link>
</Button>
)}
</>
)
}
export default PostListing

View file

@ -0,0 +1,74 @@
import * as React from "react"
import { createContext, useContext, useEffect, useState } from "react"
type Theme = "dark" | "light" | "system"
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
)
useEffect(() => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light"
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider")
return context
}

49
src/hooks/usePosts.ts Normal file
View file

@ -0,0 +1,49 @@
// @ts-nocheck
import { useEffect, useState } from "react"
const SESSION_STORAGE_KEY = "posts"
const sortPosts = (posts) => {
return posts.sort((a, b) => new Date(b.date) - new Date(a.date))
}
const usePosts = () => {
const [posts, setPosts] = useState([])
useEffect(() => {
const fetchPosts = async () => {
const storedPosts =
typeof window !== "undefined" &&
sessionStorage.getItem(SESSION_STORAGE_KEY)
if (storedPosts) {
setPosts(JSON.parse(storedPosts))
} else {
fetch("/post-index.json")
.then((res) => {
if (!res.ok) {
throw new Error(`HTTP error fetching posts. Status ${res.status}`)
}
return res.json()
})
.then((data) => {
const sortedPosts = sortPosts(data)
sessionStorage.setItem(
SESSION_STORAGE_KEY,
JSON.stringify(sortedPosts)
)
setPosts(sortedPosts)
})
.catch((err) => {
console.error("Failed to fetch posts: ", err)
})
}
}
fetchPosts()
}, [])
return { posts }
}
export { usePosts }

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

8
src/index.css Normal file
View file

@ -0,0 +1,8 @@
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap");
@import "./styles/_variables.css";
@import "./styles/shadcn-overrides.css";
@import "./styles/shadcn-theme.css";
@import "./styles/syntax-highlighting.css";
@import "tailwindcss";
@import "tw-animate-css";

6
src/lib/utils.ts Normal file
View file

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

34
src/main.tsx Normal file
View file

@ -0,0 +1,34 @@
import { StrictMode, useEffect } from "react"
import ReactDOM from "react-dom/client"
import { BrowserRouter, Routes, Route, useLocation } from "react-router"
import { HomePage } from "@/pages/home"
import { AboutPage } from "@/pages/about"
import BlogTemplate from "./templates/BlogTemplate"
import "./index.css"
import { PostsPage } from "./pages/posts"
import TagTemplate from "./templates/TagTemplate"
export default function ScrollToTop() {
const { pathname } = useLocation()
useEffect(() => {
window.scrollTo(1, 1)
}, [pathname])
return null
}
ReactDOM.createRoot(document.getElementById("root")!).render(
<StrictMode>
<BrowserRouter>
<ScrollToTop />
<Routes>
<Route index element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/posts/page/:page" element={<PostsPage />} />
<Route path="/posts/:slug" element={<BlogTemplate />} />
<Route path="/tags/:tag" element={<TagTemplate />} />
</Routes>
</BrowserRouter>
</StrictMode>
)

90
src/pages/about.tsx Normal file
View file

@ -0,0 +1,90 @@
import MainTemplate from "@/templates/MainTemplate"
import portrait from "../images/portrait-compressed.jpg"
const AboutPage = () => {
return (
<MainTemplate>
<div className="mb-5 ">
<h2 className="scroll-m-20 text-2xl font-semibold lg:text-2xl border-b pb-3">
About
</h2>
</div>
<figure className="w-full flex flex-col items-center mb-6">
<img
alt="A portrait of the blog author"
src={portrait}
className="w-0 flex"
/>
<figcaption className="text-sm text-muted-foreground mt-3 text-center">
Pictured with the WITCH computer at the{" "}
<a
href="https://www.tnmoc.org/"
target="_blank"
className="text-primary hover:text-primary/80"
>
National Museum of Computing
</a>
, Bletchley Park.
</figcaption>
</figure>
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
Another software engineer with a blog!{" "}
</p>
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
I am a self-taught engineer from London. This blog is a technical
scrapbook. I document the details of my technical life so I can have a
record of progress when I look back. Doing this publicly motivates me to
take care with my writing and to be as clear as possible.{" "}
</p>
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
Currently I work for{" "}
<a
href="https://en.wikipedia.org/wiki/ITV_(TV_network)"
target="_blank"
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
>
ITV
</a>{" "}
{""}
as a backend software engineer. Before that, I worked as a full-stack
engineer at the{" "}
<a
href="https://en.wikipedia.org/wiki/BBC"
target="_blank"
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
>
BBC
</a>{" "}
and as a frontend engineer at{" "}
<a
href="https://www.arria.com/"
target="_blank"
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
>
Arria NLG
</a>{" "}
and in several web developer roles.{" "}
</p>
<h3 className="mt-5 mb-0 scroll-m-20 text-xl font-semibold tracking-tight">
Code
</h3>
<p className="leading-[1.5] [&:not(:first-child)]:mt-6">
I self-host my own Git forge at{" "}
<a
href="https://forgejo.systemsobscure.net/thomasabishop"
className="underline decoration-1 hover:text-primary/80 underline-offset-2"
>
forgejo.systemsobscure.net
</a>{" "}
rather than use Microsoft GitHub. You can view my personal projects
there.
</p>
</MainTemplate>
)
}
export { AboutPage }

18
src/pages/home.tsx Normal file
View file

@ -0,0 +1,18 @@
import MainTemplate from "@/templates/MainTemplate"
import PostListing from "@/containers/PostListing"
import { usePosts } from "@/hooks/usePosts"
const HomePage = () => {
const { posts } = usePosts()
return (
<MainTemplate>
<PostListing
title="Recent posts"
posts={posts.slice(0, 5)}
showAllButton
/>
</MainTemplate>
)
}
export { HomePage }

131
src/pages/posts.tsx Normal file
View file

@ -0,0 +1,131 @@
// @ts-nocheck
import { useMemo, useEffect } from "react"
import MainTemplate from "@/templates/MainTemplate"
import { useParams, useNavigate } from "react-router"
import { convertDate } from "@/utils/convertDate"
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Link } from "react-router"
import { usePosts } from "@/hooks/usePosts"
const PostsPage = () => {
const { page } = useParams()
const navigate = useNavigate()
const { posts } = usePosts()
const postsPerPage = 10
const currentPage = Number(page) || 1
const totalPages = Math.ceil(posts.length / postsPerPage)
useEffect(() => {
if (currentPage < 1 || currentPage > totalPages) {
navigate(`/posts/page/1`, { replace: true })
}
}, [currentPage, totalPages, navigate])
const currentPosts = useMemo(() => {
const startIndex = (currentPage - 1) * postsPerPage
const endIndex = startIndex + postsPerPage
return posts.slice(startIndex, endIndex)
}, [posts, currentPage, postsPerPage])
const hasNextPage = currentPage < totalPages
const hasPrevPage = currentPage > 1
const goToNextPage = () => {
if (hasNextPage) {
navigate(`/posts/page/${currentPage + 1}`)
}
}
const goToPrevPage = () => {
if (hasPrevPage) {
navigate(`/posts/page/${currentPage - 1}`)
}
}
return (
<MainTemplate>
<div className="mb-5 ">
<h2 className="scroll-m-20 text-2xl font-semibold lg:text-2xl border-b pb-3">
All posts
</h2>
</div>
<div className="min-h-[calc(100vh-200px)] flex flex-col">
<div className="grid grid-cols-1 md:grid-cols-1 gap-3 flex-grow">
{currentPosts.map((post) => (
<Link
to={`/posts/${post.slug}`}
key={post.slug}
className="block no-underline"
>
<Card
key={post.slug}
className="flex flex-col h-full hover:bg-primary/5 py-4 px-0"
>
<CardHeader>
<CardTitle className="leading-snug font-semibold">
{post.title}
</CardTitle>
<CardDescription className="text-sm text-muted-foreground">
<div className="flex justify-between gap-2">
<span className="text-sm">{convertDate(post.date)}</span>
<div>
{post.tags.map((tag, i) => (
<Badge
className="ml-2 cursor-pointer"
key={i}
variant="secondary"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
navigate(`/tags/${tag}`)
}}
>
{tag}
</Badge>
))}
</div>
</div>
</CardDescription>
</CardHeader>
</Card>
</Link>
))}
</div>
<Pagination className="mt-4">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
className={`select-none ${hasPrevPage ? "cursor-pointer" : "cursor-not-allowed opacity-50"}`}
onClick={goToPrevPage}
/>
</PaginationItem>
<PaginationItem>
<PaginationNext
className={`select-none ${hasNextPage ? "cursor-pointer" : "cursor-not-allowed opacity-50"}`}
onClick={goToNextPage}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
</MainTemplate>
)
}
export { PostsPage }

71
src/styles/_variables.css Normal file
View file

@ -0,0 +1,71 @@
:root {
--radius: 0.3rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--font-monospaced: "Jetbrains Mono", monospace;
--font-sansserif: "Inter", sans-serif;
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}

View file

@ -0,0 +1,45 @@
* {
border-color: var(--border);
outline-color: color-mix(in srgb, var(--ring) 50%, transparent);
}
html {
font-family: var(--font-sansserif);
}
body {
background-color: var(--background);
color: var(--foreground);
}
button,
[type="button"],
[type="reset"],
[type="submit"],
.btn,
a[href]:not([aria-disabled="true"]),
[role="button"] {
cursor: pointer;
}
.shadow,
.shadow-sm,
.shadow-md,
.shadow-lg,
.shadow-xl,
.shadow-2xl,
[class*="shadow"] {
box-shadow: none !important;
}
.card,
.dialog,
.popover,
.dropdown-menu {
box-shadow: none !important;
}
img {
max-width: 600px;
min-width: 300px;
}

View file

@ -0,0 +1,40 @@
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius));
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}

View file

@ -0,0 +1,37 @@
code {
font-family: var(--font-monospaced);
}
p code {
color: var(--foreground);
background: var(--color-accent);
font-size: 14px;
padding: 0.2rem 0.3rem;
border-radius: var(--radius);
}
.shiki {
padding: 1rem 1.2rem;
border-radius: var(--radius);
overflow-x: auto;
margin: 1.5rem 0;
/* line-height: 1.3; */
/* counter-reset: line; */
font-family: var(--font-monospaced) !important;
font-size: 14px !important;
font-weight: 300;
}
/* .shiki .line { */
/* counter-increment: line; */
/* } */
/* .shiki .line::before { */
/* content: counter(line); */
/* color: #393a34; */
/* margin-right: 1rem; */
/* min-width: 2rem; */
/* text-align: right; */
/* display: inline-block; */
/* user-select: none; */
/* } */

View file

@ -0,0 +1,77 @@
// @ts-nocheck
import MainTemplate from "./MainTemplate"
import { Badge } from "@/components/ui/badge"
import { Link, useParams } from "react-router"
import { convertDate } from "@/utils/convertDate"
import { usePosts } from "@/hooks/usePosts"
const BlogTemplate = () => {
const { slug } = useParams()
const { posts } = usePosts()
const post = posts?.find((x) => x.slug === slug)
return (
<MainTemplate>
{!post ? (
<div>Loading...</div>
) : (
<>
<div className="mb-5">
<h2 className="text-2xl font-semibold lg:text-2xl border-b pb-3">
{post?.title}
</h2>
</div>
<div className="flex md:flex-row md:justify-between flex-col mb-8">
<span className="text-muted-foreground">
{convertDate(post?.date)}
</span>
<div className="flex gap-1 mt-3 md:mt-0">
{post?.tags?.map((tag, i) => (
<Badge asChild variant="secondary">
<Link key={i} to={`/tags/${tag}`}>
{tag}
</Link>
</Badge>
))}
</div>
</div>
<div
className="
[&>h2]:text-xl [&>h2]:border-b [&>h2]:pb-2 [&>h2]:font-semibold [&>h2]:first:mt-0 [&>h2:not(:first-child)]:mt-8
[&>h3]:text-xl [&>h3]:sm:text-1xl [&>h3]:font-semibold [&>h3:not(:first-child)]:mt-5
[&>h4]:text-lg [&>h4]:sm:text-xl [&>h4]:font-semibold [&>h4:not(:first-child)]:mt-4
[&>p]:leading-7 [&>p:not(:first-child)]:mt-6
[&>p+:is(h1,h2,h3,h4,h5,h6)]:mt-6
[&>blockquote]:mt-4 [&>blockquote]:border-l-2 [&>blockquote]:pl-6 [&>blockquote]:text-muted-foreground [&>blockquote]:text-sm
[&>ul]:my-4 [&>ul]:ml-6 [&>ul]:list-disc [&>ul>li]:mt-2
[&>table]:w-full [&>table]:my-4
[&>table>thead>tr]:m-0 [&>table>thead>tr]:border-t [&>table>thead>tr]:p-0 [&>table>thead>tr:even]:bg-muted
[&>table>thead>tr>th]:border [&>table>thead>tr>th]:px-4 [&>table>thead>tr>th]:py-2 [&>table>thead>tr>th]:text-left [&>table>thead>tr>th]:font-bold [&>table>thead>tr>th[align=center]]:text-center [&>table>thead>tr>th[align=right]]:text-right
[&>table>tbody>tr]:m-0 [&>table>tbody>tr]:border-t [&>table>tbody>tr]:p-0 [&>table>tbody>tr:even]:bg-muted
[&>table>tbody>tr>td]:border [&>table>tbody>tr>td]:px-4 [&>table>tbody>tr>td]:py-2 [&>table>tbody>tr>td]:text-left [&>table>tbody>tr>td[align=center]]:text-center [&>table>tbody>tr>td[align=right]]:text-right
[&>code]:relative [&>code]:rounded [&>code]:bg-muted [&>code]:px-[0.3rem] [&>code]:py-[0.2rem] [&>code]:font-mono [&>code]:text-sm [&>code]:font-normal
[&>table>tbody>tr:nth-child(even)]:bg-muted
[&>table>thead>tr:nth-child(even)]:bg-muted
[&_table_code]:text-sm font-normal
[&>pre]:mt-6 [&>pre]:mb-6
[&>p+pre]:mt-6
[&>pre+p]:mt-6
[&>ul+pre]:mt-6
[&>li]:leading-[1.5]
[&_li_code]:relative [&_li_code]:rounded [&_li_code]:bg-muted [&_li_code]:px-[0.3rem] [&_li_code]:py-[0.2rem] [&_li_code]:font-mono [&_li_code]:text-sm [&_li_code]:font-normal
[&>p]:mt-6 [&>p]:leading-[1.5]
[&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-muted-foreground
[&>figure]:w-full [&>figure]:flex [&>figure]:flex-col [&>figure]:items-center [&>figure]:justify-center [&>figure]:mb-6 [&>figure]:mx-auto
[&>figure>img]:w-full
[&>figure>figcaption]:text-sm [&>figure>figcaption]:text-muted-foreground [&>figure>figcaption]:mt-3 [&>figure>figcaption]:text-center
[&>figure>figcaption>a]:text-primary [&>figure>figcaption>a:hover]:text-primary/80
"
dangerouslySetInnerHTML={{ __html: post?.html }}
/>
</>
)}
</MainTemplate>
)
}
export default BlogTemplate

View file

@ -0,0 +1,32 @@
// @ts-nocheck
import { Header } from "@/components/Header"
import { ThemeProvider } from "@/context/ThemeProvider"
import { useTheme } from "@/context/ThemeProvider"
const MainTemplate = (props) => {
return (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<MainContent>{props.children}</MainContent>
</ThemeProvider>
)
}
const MainContent = ({ children }) => {
const { theme } = useTheme()
const classes =
theme === "light"
? "min-h-screen w-full flex flex-col overflow-x-hidden mb-15"
: "min-h-screen w-full flex flex-col overflow-x-hidden mb-15"
return (
<div className={classes}>
<Header />
<main className="flex-1 w-full px-2 md:px-4 flex justify-center pt-16">
<div className="w-full max-w-3xl lg:max-w-3xl xl:max-w-3xl px-4 py-3">
{children}
</div>
</main>
</div>
)
}
export default MainTemplate

View file

@ -0,0 +1,20 @@
// @ts-nocheck
import { useParams } from "react-router"
import MainTemplate from "./MainTemplate"
import PostListing from "@/containers/PostListing"
import { usePosts } from "@/hooks/usePosts"
const TagTemplate = () => {
const { tag } = useParams()
const { posts } = usePosts()
const filteredPosts = posts.filter((post) => post.tags.includes(tag))
return (
<MainTemplate>
<PostListing title={`Posts tagged: #${tag}`} posts={filteredPosts} />
</MainTemplate>
)
}
export default TagTemplate

35
src/utils/convertDate.ts Normal file
View file

@ -0,0 +1,35 @@
const months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
]
const days = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
]
const convertDate = (isoStamp: string) => {
const unixSeconds = new Date(isoStamp)
const weekday = days[unixSeconds.getDay()]
const day = unixSeconds.getDate()
const month = months[unixSeconds.getMonth()]
const year = unixSeconds.getFullYear()
return `${weekday} ${day} ${month} ${year}`
}
export { convertDate }

1
src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

37
tsconfig.app.json Normal file
View file

@ -0,0 +1,37 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"src"
]
}

22
tsconfig.json Normal file
View file

@ -0,0 +1,22 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
],
"compilerOptions": {
"strict": false,
"noImplicitAny": false,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
}
}

25
tsconfig.node.json Normal file
View file

@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

34
vite.config.ts Normal file
View file

@ -0,0 +1,34 @@
// import tailwindcss from "@tailwindcss/vite"
// import { defineConfig } from "vite"
// import react from "@vitejs/plugin-react"
// export default defineConfig({
// plugins: [react(), tailwindcss()],
// resolve: {
// alias: {
// "@": "/src",
// },
// },
// esbuild: {
// tsconfigRaw: {
// compilerOptions: {
// skipLibCheck: true,
// noEmit: true,
// },
// },
// },
// })
import tailwindcss from "@tailwindcss/vite"
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react-swc" // Changed this line
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": "/src",
},
},
// Remove esbuild config entirely when using SWC
})