feat: lodge server, client and admin

This commit is contained in:
hh committed 2026-09-19 23:33:24 +02:00
commit 95fc50e2c2
84 files changed
+7625

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
node_modules
.svelte-kit
build
.env
.env.*
+2
View File
@@ -0,0 +1,2 @@
PUBLIC_API_BASE_URL=/api
ALLOWED_HOSTS=
+23
View File
@@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# Env
.env
.env.*
!.env.example
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Impeccable
**/.impeccable/config.local.json
**/.impeccable/hook.cache.json
**/.impeccable/hook.pending.json
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+6
View File
@@ -0,0 +1,6 @@
## Checking commands
After writing code, always run from `frontend/`:
```shell
bun fix
bun check
```
+26
View File
@@ -0,0 +1,26 @@
COMPOSE := cd .. && docker compose
changed = ! git diff --quiet $(1) -- $(2) 2>/dev/null
stamp = { git update-ref $(1) HEAD 2>/dev/null || true; }
.PHONY: fmt check build pre-deploy post-deploy dev
fmt:
bun run fix
check:
bun run check
bun run lint
build:
$(COMPOSE) run --rm frontend-dev sh -c "bun install && bun run build"
pre-deploy:
@if $(call changed,refs/deploy/frontend,.) || ! test -d build; then \
$(MAKE) build && $(call stamp,refs/deploy/frontend); \
fi
post-deploy:
@:
dev:
bun run dev
+42
View File
@@ -0,0 +1,42 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"extends": ["ultracite/biome/core", "ultracite/biome/svelte"],
"files": {
"includes": [
"!.claude",
"!.svelte-kit",
"!build",
"!.mcp.json",
"!.impeccable"
]
},
"javascript": { "globals": ["Bun"] },
"overrides": [
{
"includes": ["src/lib/components/ui/**"],
"linter": { "enabled": false },
"assist": { "enabled": false }
},
{
"includes": ["src/lib/utils.ts"],
"linter": { "rules": { "suspicious": { "noExplicitAny": "off" } } }
},
{
"includes": ["**/*.svelte"],
"linter": {
"rules": {
"style": {
"useFilenamingConvention": {
"level": "error",
"options": { "filenameCases": ["kebab-case", "PascalCase"] }
}
}
}
}
},
{
"includes": ["src/app.d.ts"],
"linter": { "rules": { "style": { "noNamespace": "off" } } }
}
]
}
+583
View File
@@ -0,0 +1,583 @@
{
"lockfileVersion": 2,
"configVersion": 1,
"workspaces": {
"": {
"name": "lodge",
"devDependencies": {
"@biomejs/biome": "2.5.2",
"@fontsource-variable/archivo": "^5.3.0",
"@fontsource-variable/jetbrains-mono": "^5.3.0",
"@internationalized/date": "^3.12.0",
"@lucide/svelte": "^1.23.0",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.63.0",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@tailwindcss/vite": "^4.3.0",
"@types/bun": "^1.3.0",
"bits-ui": "^2.16.3",
"clsx": "^2.1.1",
"diff": "^9.0.0",
"dompurify": "^3.4.15",
"marked": "^18.0.13",
"mode-watcher": "^1.1.0",
"shadcn-svelte": "^1.3.0",
"svelte": "^5.56.1",
"svelte-check": "^4.6.0",
"svelte-sonner": "^1.1.0",
"tailwind-merge": "^3.5.0",
"tailwind-variants": "^3.2.2",
"tailwindcss": "^4.3.0",
"tw-animate-css": "^1.4.0",
"typescript": "^6.0.3",
"ultracite": "7.9.0",
"vite": "^8.0.16",
},
},
},
"packages": {
"@biomejs/biome": ["@biomejs/biome@2.5.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.2", "@biomejs/cli-darwin-x64": "2.5.2", "@biomejs/cli-linux-arm64": "2.5.2", "@biomejs/cli-linux-arm64-musl": "2.5.2", "@biomejs/cli-linux-x64": "2.5.2", "@biomejs/cli-linux-x64-musl": "2.5.2", "@biomejs/cli-win32-arm64": "2.5.2", "@biomejs/cli-win32-x64": "2.5.2" }, "bin": { "biome": "bin/biome" } }, "sha512-VQ3RCqr7JmDIX+w6stWYl+g/3bYofN3q2wDBHUKKc/c7i5QWrFKFBZYCYPWTE6agsUPMIZZe6/CMmVUfUAhkKA=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-e7P3P7EkwFc/KiX2AHw4YDLIBOMfG9CPCAwy52k5Bp0dfhkozx9hf6wCmIr2QeXy2XeccJ3V/Sg+hDmzYEqxSg=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ymzMvjC1Jg0b9K0D26ZdARqFQXs7MocfLC5FOCGfkC0Ss+ACUJkX5364ZM5nT4NLZanHRZNVrZEy+Ibwcvux/g=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-t7sseOmqND57uUWTwlawU6BYj+J06T/9EkydzBhkrgw/FK3QVhjU2wsJR0frljrKZ0/I8A/rYw7284QgqjQfIQ=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-w+ANG0ZvTu9IeEg9QnstoOnk6L0fpwJifW6aHR18+cb5Z39bkANItYjAfMrnvce5tmMK+IQ6nPX7/kQFdam5iw=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-M/lOZrewzTCRDINbjhQ1gYYru37KlD3kJBQwwKCG0ckz5E9IZwIoJ3X0wBwRXA+yBDIwWUuPBHS67HzJY4dTfA=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-VArNLAzND063tF+XY0yPyM+DyahpzOMzOAvb7qs259nhjJWRjvjZdssuA+Rfl+l07+NOesKZ0Xu2yFrXyBMtzw=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-kbjFFKyZlzYnAuw7sRy5qDoFG6zrP40UK08oPQsWK0ct3NMnGSt+Bs1iviEEyEIP57N5MrykGXdO/wRiaR4lww=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-4InchVpdVmdkkkgjQqKpgvyu+VPnoF/7RPSw5YATgEVpt2j72wcCAeV5TwaE9ZGJUZWZn7v2CwSAj6CrMJEx8A=="],
"@cacheable/memory": ["@cacheable/memory@2.2.0", "", { "dependencies": { "@cacheable/utils": "^2.5.0", "@keyv/bigmap": "^1.3.1", "hookified": "^1.15.1", "keyv": "^5.6.0" } }, "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ=="],
"@cacheable/utils": ["@cacheable/utils@2.5.0", "", { "dependencies": { "hashery": "^1.5.1", "keyv": "^5.6.0" } }, "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA=="],
"@clack/core": ["@clack/core@1.5.1", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ=="],
"@clack/prompts": ["@clack/prompts@1.8.1", "", { "dependencies": { "@clack/core": "1.5.1", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
"@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.7.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw=="],
"@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
"@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.3", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q=="],
"@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
"@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
"@fontsource-variable/archivo": ["@fontsource-variable/archivo@5.3.0", "", {}, "sha512-HogK8FJelrD1o7TlZlkIVtHgc20bO5PZRWE7mUeUTdMN055alznQV6/00J00IBeu8FQAH4s3zW9UJNvKExXf+g=="],
"@fontsource-variable/jetbrains-mono": ["@fontsource-variable/jetbrains-mono@5.3.0", "", {}, "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw=="],
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
"@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@internationalized/date": ["@internationalized/date@3.12.4", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@keyv/bigmap": ["@keyv/bigmap@1.3.1", "", { "dependencies": { "hashery": "^1.4.0", "hookified": "^1.15.0" }, "peerDependencies": { "keyv": "^5.6.0" } }, "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ=="],
"@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="],
"@lucide/svelte": ["@lucide/svelte@1.47.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-tpuE7JIK9FzYDWsJx6PxHj9ROyCpnDxZfvHu5q5yHGKS2CFNjzSyf1o9/Ypu2A27GLvkPGqZcIMrF6idM+eQZQ=="],
"@oxc-project/types": ["@oxc-project/types@0.150.0", "", {}, "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw=="],
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
"@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.9", "", { "os": "android", "cpu": "arm" }, "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.9", "", { "os": "android", "cpu": "arm64" }, "sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.9", "", { "os": "linux", "cpu": "arm" }, "sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.9", "", { "os": "linux", "cpu": "x64" }, "sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.9", "", { "os": "linux", "cpu": "x64" }, "sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.9", "", { "os": "none", "cpu": "arm64" }, "sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.9", "", { "os": "win32", "cpu": "x64" }, "sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.13", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ=="],
"@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="],
"@sveltejs/kit": ["@sveltejs/kit@2.70.3", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg=="],
"@sveltejs/load-config": ["@sveltejs/load-config@0.2.3", "", {}, "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@7.3.0", "", { "dependencies": { "deepmerge": "^4.3.1", "magic-string": "^1.0.0", "obug": "^2.1.0", "vitefu": "^1.1.2" }, "peerDependencies": { "svelte": "^5.46.4", "vite": "^8.0.0-beta.7 || ^8.0.0" } }, "sha512-QbRoJyD92e9R0ufeQIWRHrCC0ObcqSv/aBDdrQMoU+sypav3cDx5wytdQ6GLdXjEMO6xjrXGzfkUygng8JMv0A=="],
"@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="],
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="],
"@types/bun": ["@types/bun@1.4.2", "", { "dependencies": { "bun-types": "1.4.2" } }, "sha512-GimotNn7+ZV0uVArItBbriZsR1oNf0+WTzPkdcFrzShI7k2norL0uzEaJT8T33dWr7O/c9ZDuAFQrctKCi72oQ=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@26.6.2", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.70.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.70.0", "@typescript-eslint/types": "^8.70.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.70.0", "", { "dependencies": { "@typescript-eslint/types": "8.70.0", "@typescript-eslint/visitor-keys": "8.70.0" } }, "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.70.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.70.0", "", {}, "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.70.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.70.0", "@typescript-eslint/tsconfig-utils": "8.70.0", "@typescript-eslint/types": "8.70.0", "@typescript-eslint/visitor-keys": "8.70.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.70.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.70.0", "@typescript-eslint/types": "8.70.0", "@typescript-eslint/typescript-estree": "8.70.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.70.0", "", { "dependencies": { "@typescript-eslint/types": "8.70.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ=="],
"acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"bits-ui": ["bits-ui@2.19.2", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-W9upilTmLTIWjuVAD6Ta7XoS6gxRflmCQnXamj+WN1eYxZecghqGENpuSMAKQmVENMLXhh85BZb04LCrBd2+Ng=="],
"brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="],
"bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="],
"cacheable": ["cacheable@2.5.0", "", { "dependencies": { "@cacheable/memory": "^2.2.0", "@cacheable/utils": "^2.5.0", "hookified": "^1.15.0", "keyv": "^5.6.0", "qified": "^0.10.1" } }, "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"cn": ["cn@0.3.0", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-qRoU0ARrZD3Bbj1wKFftEKxPjSnzTEdFkf22NVhW+zU9D/wajhoP+qUZ5DChhhSOBCH1onV12hrmGqfu1+5Z2Q=="],
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"devalue": ["devalue@5.9.4", "", {}, "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg=="],
"diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
"dompurify": ["dompurify@3.4.15", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw=="],
"enhanced-resolve": ["enhanced-resolve@5.25.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@10.11.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "11.1.5 || >11.1.6 <12", "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", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ=="],
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
"eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
"espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
"esrap": ["esrap@2.3.7", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-n2nf7fZR3c9yXf0BPEuHuXqT+KW0SJVj4cN5FMEkpCZ3scLjOQWpiccyCxVzCC2q1wubTghuEGzngJY/7Ah0Ow=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
"fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
"fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"file-entry-cache": ["file-entry-cache@11.1.5", "", { "dependencies": { "flat-cache": "^6.1.23" } }, "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"flat-cache": ["flat-cache@6.1.23", "", { "dependencies": { "cacheable": "^2.5.0", "flatted": "^3.4.2", "hookified": "^1.15.0" } }, "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA=="],
"flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"hashery": ["hashery@1.5.1", "", { "dependencies": { "hookified": "^1.15.0" } }, "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ=="],
"hookified": ["hookified@1.15.1", "", {}, "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
"keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="],
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"marked": ["marked@18.0.13", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-xTxVzZsBFwunP6HDmtBkabUQEYArnP7/rMDGmPj9SlrKlQ4i8MdYVow+nJL0eOqwpUqhzBoTBRADGN6uYwPyOw=="],
"minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="],
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"mode-watcher": ["mode-watcher@1.1.0", "", { "dependencies": { "runed": "^0.25.0", "svelte-toolbelt": "^0.7.1" }, "peerDependencies": { "svelte": "^5.27.0" } }, "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g=="],
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
"nypm": ["nypm@0.6.10", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.3.1" }, "bin": { "nypm": "./dist/cli.mjs" } }, "sha512-W72Hrj1petq+b3Hk2aAC+9zswetlIFTnDW4s5djseZh2nYqBbyQLOtj472HwcbcWykPBUW1WpWpjOd4nK6gMRw=="],
"obug": ["obug@2.2.1", "", {}, "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
"postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"qified": ["qified@0.10.1", "", { "dependencies": { "hookified": "^2.1.1" } }, "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"rolldown": ["rolldown@1.2.9", "", { "dependencies": { "@oxc-project/types": "=0.150.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.9", "@rolldown/binding-android-arm64": "1.2.9", "@rolldown/binding-darwin-arm64": "1.2.9", "@rolldown/binding-darwin-x64": "1.2.9", "@rolldown/binding-freebsd-x64": "1.2.9", "@rolldown/binding-linux-arm-gnueabihf": "1.2.9", "@rolldown/binding-linux-arm64-gnu": "1.2.9", "@rolldown/binding-linux-arm64-musl": "1.2.9", "@rolldown/binding-linux-ppc64-gnu": "1.2.9", "@rolldown/binding-linux-s390x-gnu": "1.2.9", "@rolldown/binding-linux-x64-gnu": "1.2.9", "@rolldown/binding-linux-x64-musl": "1.2.9", "@rolldown/binding-openharmony-arm64": "1.2.9", "@rolldown/binding-win32-arm64-msvc": "1.2.9", "@rolldown/binding-win32-x64-msvc": "1.2.9" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg=="],
"runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="],
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
"semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="],
"shadcn-svelte": ["shadcn-svelte@1.7.0", "", { "dependencies": { "cn": "^0.3.0", "commander": "^14.0.0", "node-fetch-native": "^1.6.4", "postcss": "^8.5.10" }, "peerDependencies": { "svelte": "^5.0.0" }, "bin": { "shadcn-svelte": "dist/index.mjs" } }, "sha512-AK3ZsYOL8PN/RxcEBCHGvQj2TrLfaaBsbw0QohyEb3nn/yPNDRC/+9iSjnTp/uKEe+tAi0tBxB/Yhzno0N7EEg=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
"svelte": ["svelte@5.57.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@jridgewell/sourcemap-codec": "^1.6.0", "@sveltejs/acorn-typescript": "^1.0.13", "@types/estree": "^1.0.9", "acorn": "^8.18.0", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.9.2", "esm-env": "^1.2.1", "esrap": "^2.3.6", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-Uqj49lWKB+iSSnneuwiYYJ7MZgkB+eXr0LXBhv4uDuAkXqnWmq65Sxflfvp0Lc6MdKjMUxGaeOKWJqz5SNiVIA=="],
"svelte-check": ["svelte-check@4.7.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.3", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw=="],
"svelte-sonner": ["svelte-sonner@1.2.1", "", { "dependencies": { "runed": "^0.28.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-N00LOWb60fjKXULbpngku+7E9Krb16KpNq4/Eax5Vu7o1Dpbca55QpaLPsB+yp82JIEdFhfJuv5cVXLoe0ZtVA=="],
"svelte-toolbelt": ["svelte-toolbelt@0.10.6", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.35.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ=="],
"tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="],
"tailwind-merge": ["tailwind-merge@3.7.0", "", {}, "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA=="],
"tailwind-variants": ["tailwind-variants@3.3.1", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge", "tailwindcss"] }, "sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw=="],
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"tinyexec": ["tinyexec@1.3.1", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"ultracite": ["ultracite@7.9.0", "", { "dependencies": { "@clack/prompts": "^1.5.1", "@typescript-eslint/utils": "^8.62.1", "commander": "^15.0.0", "cross-spawn": "^7.0.6", "deepmerge": "^4.3.1", "glob": "^13.0.6", "jsonc-parser": "^3.3.1", "nypm": "^0.6.6", "yaml": "^2.9.0", "zod": "^4.4.3" }, "peerDependencies": { "oxfmt": ">=0.1.0", "oxlint": "^1.0.0" }, "optionalPeers": ["oxfmt", "oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-Lkh6MXuX/vI89jeDIaEiOifI1BVXEJmk6e+igKRGAzdp5jKzeFa5d5A0tpJmHGCxXzd42jVwNpcs7jqm87IfNg=="],
"undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"vite": ["vite@8.3.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ=="],
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zimmerframe": ["zimmerframe@1.1.5", "", {}, "sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA=="],
"zod": ["zod@4.6.5", "", {}, "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q=="],
"@sveltejs/vite-plugin-svelte/magic-string": ["magic-string@1.4.1", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow=="],
"@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"eslint/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"espree/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"mode-watcher/runed": ["runed@0.25.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg=="],
"mode-watcher/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
"qified/hookified": ["hookified@2.2.0", "", {}, "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA=="],
"svelte-sonner/runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="],
"ultracite/commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="],
"@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"mode-watcher/svelte-toolbelt/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="],
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"tailwind": {
"css": "src/routes/layout.css",
"baseColor": "neutral"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks",
"lib": "$lib"
},
"typescript": true,
"registry": "https://shadcn-svelte.com/registry",
"style": "luma",
"iconLibrary": "lucide",
"menuColor": "default-translucent",
"menuAccent": "subtle"
}
@@ -0,0 +1,11 @@
services:
frontend-dev:
networks:
default: {}
caddy:
aliases:
- lodge-frontend
networks:
caddy:
external: true
+22
View File
@@ -0,0 +1,22 @@
services:
frontend-dev:
image: oven/bun:alpine
profiles: [frontend-dev]
restart: unless-stopped
working_dir: /app
env_file:
- path: .env
required: false
- path: frontend/.env
required: false
environment:
API_PROXY_TARGET: http://api:8080
volumes:
- ./frontend:/app
- frontend_node_modules:/app/node_modules
- frontend_svelte_kit:/app/.svelte-kit
command: [sh, -c, "bun install && bun run dev --host 0.0.0.0 --port 3000"]
volumes:
frontend_node_modules:
frontend_svelte_kit:
+45
View File
@@ -0,0 +1,45 @@
{
"name": "lodge",
"type": "module",
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "ultracite check",
"fix": "ultracite fix"
},
"devDependencies": {
"@biomejs/biome": "2.5.2",
"@fontsource-variable/archivo": "^5.3.0",
"@fontsource-variable/jetbrains-mono": "^5.3.0",
"@internationalized/date": "^3.12.0",
"@lucide/svelte": "^1.23.0",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.63.0",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@tailwindcss/vite": "^4.3.0",
"@types/bun": "^1.3.0",
"bits-ui": "^2.16.3",
"clsx": "^2.1.1",
"diff": "^9.0.0",
"dompurify": "^3.4.15",
"marked": "^18.0.13",
"mode-watcher": "^1.1.0",
"shadcn-svelte": "^1.3.0",
"svelte": "^5.56.1",
"svelte-check": "^4.6.0",
"svelte-sonner": "^1.1.0",
"tailwind-merge": "^3.5.0",
"tailwind-variants": "^3.2.2",
"tailwindcss": "^4.3.0",
"tw-animate-css": "^1.4.0",
"typescript": "^6.0.3",
"ultracite": "7.9.0",
"vite": "^8.0.16"
}
}
+13
View File
@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta content="scale" name="text-scale">
<link href="%sveltekit.assets%/favicon.svg" rel="icon" type="image/svg+xml">
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+54
View File
@@ -0,0 +1,54 @@
import { env } from "$env/dynamic/public";
const BASE = env.PUBLIC_API_BASE_URL ?? "/api";
export class ApiError extends Error {
status: number;
constructor(status: number, message: string) {
super(message);
this.status = status;
this.name = "ApiError";
}
}
type Json = Record<string, unknown> | unknown[];
const send = async (
method: string,
path: string,
body?: Json
): Promise<Response> => {
const init: RequestInit = { credentials: "include", method };
if (body !== undefined) {
init.headers = { "Content-Type": "application/json" };
init.body = JSON.stringify(body);
}
const res = await fetch(BASE + path, init);
if (!res.ok) {
const detail = (await res.text()).trim();
throw new ApiError(res.status, detail || res.statusText);
}
return res;
};
const request = async <T>(
method: string,
path: string,
body?: Json
): Promise<T> => {
const res = await send(method, path, body);
if (res.status === 204) {
return undefined as T;
}
return (await res.json()) as T;
};
export const api = {
bytes: async (path: string) =>
new Uint8Array(await (await send("GET", path)).arrayBuffer()),
del: <T>(path: string) => request<T>("DELETE", path),
events: () => new EventSource(`${BASE}/events`, { withCredentials: true }),
get: <T>(path: string) => request<T>("GET", path),
post: <T>(path: string, body?: Json) => request<T>("POST", path, body),
};
+253
View File
@@ -0,0 +1,253 @@
<script lang="ts">
import { diffLines } from "diff";
import DOMPurify from "dompurify";
import { marked } from "marked";
interface Props {
before: string | null;
markdown: boolean;
now: string;
view: string;
}
interface Row {
kind: "added" | "gap" | "removed" | "same";
text: string;
}
const { before, markdown, now, view }: Props = $props();
const CONTEXT = 3;
const TRAILING_NEWLINE = /\n$/;
const html = $derived(
markdown && view === "read"
? DOMPurify.sanitize(marked.parse(now, { async: false }))
: ""
);
const lines = (value: string) =>
value.replace(TRAILING_NEWLINE, "").split("\n");
const rows = $derived.by(() => {
const result: Row[] = [];
for (const part of diffLines(before ?? "", now)) {
const partLines = lines(part.value);
if (part.added || part.removed) {
const kind = part.added ? "added" : "removed";
result.push(...partLines.map((text): Row => ({ kind, text })));
continue;
}
if (partLines.length <= CONTEXT * 2 + 1) {
result.push(...partLines.map((text): Row => ({ kind: "same", text })));
continue;
}
const hidden = partLines.length - CONTEXT * 2;
result.push(
...partLines
.slice(0, CONTEXT)
.map((text): Row => ({ kind: "same", text })),
{ kind: "gap", text: `${hidden} unchanged lines` },
...partLines
.slice(-CONTEXT)
.map((text): Row => ({ kind: "same", text }))
);
}
return result;
});
</script>
{#if view === "changes"}
<ol class="diff mono">
{#each rows as row, index (index)}
<li class={row.kind}>
{#if row.kind === "removed"}
<del>{row.text || " "}</del>
{:else if row.kind === "added"}
<ins>{row.text || " "}</ins>
{:else}
{row.text || " "}
{/if}
</li>
{/each}
</ol>
{:else if html}
<article class="prose">
{@html html}
</article>
{:else}
<pre class="source mono">{now}</pre>
{/if}
<style>
.prose {
max-width: 70ch;
font-size: 1.0625rem;
line-height: 1.65;
overflow-wrap: break-word;
}
.prose :global(:is(h1, h2, h3, h4)) {
margin: 2.25em 0 0.6em;
font-weight: 700;
font-stretch: 85%;
line-height: 1.2;
text-wrap: balance;
}
.prose :global(h1) {
font-size: 1.625rem;
}
.prose :global(h2) {
font-size: 1.3125rem;
}
.prose :global(h3) {
font-size: 1.125rem;
}
.prose :global(:first-child) {
margin-top: 0;
}
.prose :global(:is(p, ul, ol, pre, blockquote, table)) {
margin: 0 0 1.1em;
}
.prose :global(:is(ul, ol)) {
padding-left: 1.4em;
}
.prose :global(ul) {
list-style: square;
}
.prose :global(ol) {
list-style: decimal;
}
.prose :global(li) {
margin: 0.25em 0;
}
.prose :global(li::marker) {
color: var(--green);
}
.prose :global(code) {
padding: 0.1em 0.3em;
font-family: var(--mono);
font-size: 0.8125em;
background: var(--paper-2);
}
.prose :global(pre) {
padding: 0.875rem 1rem;
overflow-x: auto;
line-height: 1.55;
background: var(--paper-2);
}
.prose :global(pre code) {
padding: 0;
background: none;
}
.prose :global(blockquote) {
position: relative;
padding-left: 1.25em;
color: var(--ink-2);
}
.prose :global(blockquote::before) {
position: absolute;
inset: 0.2em auto 0.2em 0;
width: 1px;
content: "";
background: var(--ochre);
}
.prose :global(hr) {
margin: 2em 0;
border: 0;
border-top: 1px solid var(--rule);
}
.prose :global(table) {
display: block;
overflow-x: auto;
font-variant-numeric: tabular-nums;
border-collapse: collapse;
}
.prose :global(:is(th, td)) {
padding: 0.4em 1em 0.4em 0;
text-align: left;
border-bottom: 1px solid var(--rule);
}
.prose :global(img) {
max-width: 100%;
}
.source,
.diff {
overflow-x: auto;
line-height: 1.6;
tab-size: 2;
white-space: pre;
}
.diff li {
position: relative;
min-width: max-content;
padding: 0 0.75rem 0 1.75rem;
}
.diff .same {
color: var(--ink-2);
}
.diff .added {
background: var(--green-wash);
}
.diff .removed {
background: var(--red-wash);
}
.diff .added::before,
.diff .removed::before {
position: absolute;
left: 0.5rem;
font-weight: 700;
}
.diff .added::before {
color: var(--green);
content: "+";
}
.diff .removed::before {
color: var(--red);
content: "−";
}
.diff ins {
text-decoration: none;
}
.diff del {
text-decoration-color: var(--red);
}
.diff .gap {
padding-block: 0.25rem;
margin: 0.5rem 0;
font-family: var(--sans);
font-size: 0.8125rem;
color: var(--ink-2);
border-block: 1px dashed var(--rule);
}
</style>
+288
View File
@@ -0,0 +1,288 @@
<script lang="ts">
import { ago, lithology, type Revision, shortSha } from "$lib/lodge";
interface Props {
at: number;
fresh: number;
machines: string[];
onselect: (seq: number, scrubbing: boolean) => void;
revisions: Revision[];
touched: Set<number> | null;
}
const { at, fresh, machines, onselect, revisions, touched }: Props = $props();
const BASE_HEIGHT = 20;
const CHANGE_HEIGHT = 6;
const MAX_CHANGES = 6;
const NUMERAL_EVERY = 5;
let column = $state<HTMLElement | undefined>();
let scrubbing = $state(false);
const height = (rev: Revision) =>
BASE_HEIGHT + Math.min(rev.changes.length, MAX_CHANGES) * CHANGE_HEIGHT;
const numbered = (rev: Revision, index: number) =>
rev.seq === at ||
rev.seq % NUMERAL_EVERY === 0 ||
index === 0 ||
index === revisions.length - 1;
const formation = (rev: Revision, index: number) =>
index === 0 || revisions[index - 1].main_sha !== rev.main_sha;
const describeBed = (rev: Revision) =>
`Revision ${rev.seq}, ${rev.machine}, ${rev.changes.length} changed, ${rev.main_branch || "detached"} ${shortSha(rev.main_sha)}, ${ago(rev.time)}`;
const seqAt = (x: number, y: number): number | null => {
const bed = document.elementFromPoint(x, y)?.closest("[data-seq]");
return bed instanceof HTMLElement ? Number(bed.dataset.seq) : null;
};
const press = (event: PointerEvent) => {
const seq = seqAt(event.clientX, event.clientY);
if (seq === null || event.button !== 0) {
return;
}
if (event.pointerType === "mouse" || seq === at) {
scrubbing = true;
column?.setPointerCapture(event.pointerId);
onselect(seq, true);
}
};
const drag = (event: PointerEvent) => {
if (!(scrubbing && column)) {
return;
}
const bounds = column.getBoundingClientRect();
const seq = seqAt(bounds.left + bounds.width / 2, event.clientY);
if (seq !== null && seq !== at) {
onselect(seq, true);
}
};
const release = () => {
if (scrubbing) {
scrubbing = false;
onselect(at, false);
}
};
const step = (event: KeyboardEvent) => {
const move = { ArrowDown: 1, ArrowUp: -1, j: 1, k: -1 }[event.key];
if (!move) {
return;
}
event.preventDefault();
const index = revisions.findIndex((rev) => rev.seq === at) + move;
const next = revisions[Math.max(0, Math.min(revisions.length - 1, index))];
onselect(next.seq, false);
column?.querySelector<HTMLElement>(`[data-seq="${next.seq}"]`)?.focus();
};
$effect(() => {
column
?.querySelector(`[data-seq="${at}"]`)
?.scrollIntoView({ block: "nearest" });
});
</script>
<div
aria-label="Revision history, newest at the top"
aria-orientation="vertical"
class="column"
onkeydown={step}
onpointercancel={release}
onpointerdown={press}
onpointermove={drag}
onpointerup={release}
role="listbox"
tabindex="-1"
bind:this={column}
class:scrubbing
>
{#each revisions as rev, index (rev.seq)}
<button
aria-label={describeBed(rev)}
aria-selected={rev.seq === at}
class="bed"
data-seq={rev.seq}
onclick={() => onselect(rev.seq, false)}
role="option"
title="{rev.machine} · {ago(rev.time)}"
type="button"
style:height="{height(rev)}px"
class:above={rev.seq > at}
class:fresh={rev.seq === fresh}
class:quiet={touched !== null && !touched.has(rev.seq)}
>
<span class="depth stamp">{numbered(rev, index) ? rev.seq : ""}</span>
<span class="fill {lithology(rev.machine, machines)}"></span>
<span class="mark"></span>
<span class="formation">
{#if formation(rev, index)}
<span class="branch">{rev.main_branch || "detached"}</span>
<span class="mono">{shortSha(rev.main_sha)}</span>
{/if}
</span>
</button>
{/each}
</div>
<style>
.column {
display: flex;
flex-direction: column;
padding-bottom: 3rem;
overflow-y: auto;
overscroll-behavior: contain;
user-select: none;
scrollbar-width: none;
background-color: var(--paper-2);
background-image:
linear-gradient(var(--grid-strong) 1px, transparent 1px),
linear-gradient(var(--grid) 1px, transparent 1px),
linear-gradient(90deg, var(--grid) 1px, transparent 1px);
background-size:
100% 40px,
100% 8px,
8px 100%;
}
.column.scrubbing {
cursor: grabbing;
}
.bed {
position: relative;
display: grid;
flex: none;
grid-template-columns: 2.75rem 4.5rem 0.5rem 1fr;
align-items: stretch;
padding: 0;
color: var(--ink);
text-align: left;
touch-action: pan-y;
background: none;
border: 0;
border-top: 1px solid var(--rule);
}
.bed.fresh {
animation: settle 600ms var(--ease);
}
.depth {
padding-right: 0.5rem;
font-size: 0.8125rem;
line-height: 1.25rem;
color: var(--ink-2);
text-align: right;
}
.fill {
border-inline: 1px solid var(--ink);
transition: opacity 150ms var(--ease);
}
.bed.quiet .fill {
--l: var(--ink-2);
}
.bed.quiet .fill::after {
opacity: 0.6;
}
.bed:not(.quiet) .mark {
background: var(--green);
}
.column:not(:has(.quiet)) .mark {
background: none;
}
.bed.above .fill::after {
display: none;
}
.formation {
display: flex;
gap: 0.375rem;
min-width: 0;
padding-left: 0.375rem;
font-size: 0.75rem;
line-height: 1.25rem;
color: var(--ink-2);
white-space: nowrap;
}
.branch {
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
font-stretch: 80%;
color: var(--ink);
}
.bed:hover .fill {
--l: var(--green);
}
.bed[aria-selected="true"] {
touch-action: none;
}
.bed[aria-selected="true"]::before {
position: absolute;
inset: -1px 0 auto;
z-index: 1;
height: 3px;
content: "";
background: var(--red);
}
.bed[aria-selected="true"]::after {
position: absolute;
top: -1.375rem;
right: 0;
z-index: 3;
width: 0.625rem;
height: 2.75rem;
content: "";
background: var(--red);
}
.bed:first-child[aria-selected="true"]::after {
top: 0;
}
.bed:first-child[aria-selected="true"]::before {
top: 0;
}
.bed[aria-selected="true"] .depth {
color: var(--red);
}
.bed:focus-visible {
z-index: 2;
outline-offset: -2px;
}
@media (max-width: 56rem) {
.bed {
grid-template-columns: 1.75rem 1fr 0.25rem;
}
.formation {
display: none;
}
.depth {
padding-right: 0.25rem;
font-size: 0.6875rem;
}
}
</style>
+2
View File
@@ -0,0 +1,2 @@
// place files you want to import through the `$lib` alias in this folder.
// biome-ignore-all lint/suspicious/noEmptySource: temporary
+136
View File
@@ -0,0 +1,136 @@
import { api } from "$lib/api/client";
export interface Change {
base: string;
hash: string;
path: string;
space: string;
}
export interface Revision {
changes: Change[];
machine: string;
main_branch: string;
main_sha: string;
seq: number;
time: number;
}
export interface Repo {
key: string;
name: string;
seq: number;
time: number;
}
export interface Machine {
created_at: number;
id: number;
last_push: number;
name: string;
revisions: number;
}
export interface FileEntry {
hash: string;
time: number;
}
export interface RepoState {
seq: number;
spaces: Record<string, Record<string, FileEntry>>;
}
export interface Code {
code: string;
command: string;
expires_at: number;
}
export const SHARED = "shared";
const PATTERNS = 7;
const WEB = "web";
const REVOKED = "revoked";
const SHORT_SHA = 7;
const BRANCH_PREFIX = "branch:";
export const lithology = (machine: string, machines: string[]): string => {
if (machine === WEB) {
return "lith lith-web";
}
const index = machines.indexOf(machine);
if (machine === REVOKED || index < 0) {
return "lith lith-gone";
}
return `lith lith-${index % PATTERNS}`;
};
export const shortSha = (sha: string): string =>
sha ? sha.slice(0, SHORT_SHA) : "no commit";
export const spaceName = (space: string): string =>
space.startsWith(BRANCH_PREFIX) ? space.slice(BRANCH_PREFIX.length) : space;
export const fileId = (space: string, path: string): string =>
`${space}\n${path}`;
export const changeKind = (
change: Change
): "added" | "deleted" | "modified" => {
if (!change.base) {
return "added";
}
return change.hash ? "modified" : "deleted";
};
const UNITS: [Intl.RelativeTimeFormatUnit, number][] = [
["year", 31_536_000_000],
["month", 2_592_000_000],
["day", 86_400_000],
["hour", 3_600_000],
["minute", 60_000],
];
const relative = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
const absolute = new Intl.DateTimeFormat("en-GB", {
dateStyle: "medium",
timeStyle: "short",
});
export const ago = (time: number): string => {
if (!time) {
return "never";
}
const delta = time - Date.now();
for (const [unit, size] of UNITS) {
if (Math.abs(delta) >= size) {
return relative.format(Math.round(delta / size), unit);
}
}
return "just now";
};
export const exact = (time: number): string => absolute.format(time);
const blobs = new Map<string, Promise<Uint8Array>>();
export const blob = (hash: string): Promise<Uint8Array> => {
let pending = blobs.get(hash);
if (!pending) {
pending = api.bytes(`/blobs/${hash}`);
blobs.set(hash, pending);
}
return pending;
};
const decoder = new TextDecoder("utf-8", { fatal: true });
export const text = (bytes: Uint8Array): string | null => {
if (bytes.includes(0)) {
return null;
}
try {
return decoder.decode(bytes);
} catch {
return null;
}
};
+32
View File
@@ -0,0 +1,32 @@
import { goto } from "$app/navigation";
import { ApiError, api } from "$lib/api/client";
export const session = $state({ signedIn: false });
export const requireSession = async (): Promise<void> => {
try {
await api.get("/session");
session.signedIn = true;
} catch {
session.signedIn = false;
await goto("/login");
}
};
export const signOut = async (): Promise<void> => {
await api.del("/session");
session.signedIn = false;
await goto("/login");
};
export const describe = (error: unknown): string => {
if (error instanceof ApiError && error.status === 401) {
session.signedIn = false;
goto("/login");
return "Your session ended. Sign in again.";
}
if (error instanceof ApiError) {
return error.message;
}
return "The server did not answer. Check that lodge-server is running and try again.";
};
+15
View File
@@ -0,0 +1,15 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
export type WithoutChildren<T> = T extends { children?: any }
? Omit<T, "children">
: T;
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & {
ref?: U | null;
};
+110
View File
@@ -0,0 +1,110 @@
<script lang="ts">
import "./layout.css";
import { ModeWatcher, mode, toggleMode } from "mode-watcher";
import { onMount } from "svelte";
import { page } from "$app/state";
import { requireSession, session, signOut } from "$lib/session.svelte";
let { children } = $props();
const onLogin = $derived(page.url.pathname === "/login");
onMount(() => {
if (!onLogin) {
requireSession();
}
});
</script>
<ModeWatcher />
{#if onLogin}
{@render children()}
{:else if session.signedIn}
<header class="bar">
<a class="wordmark" href="/">lodge</a>
<nav aria-label="Sections">
<a aria-current={page.url.pathname === "/" ? "page" : undefined} href="/"
>Repositories</a
>
<a
aria-current={page.url.pathname === "/machines" ? "page" : undefined}
href="/machines"
>Machines</a
>
</nav>
<div class="tools">
<button class="plain" onclick={toggleMode} type="button">
{mode.current === "dark" ? "Day print" : "Night print"}
</button>
<button class="plain" onclick={signOut} type="button">Sign out</button>
</div>
</header>
{@render children()}
{/if}
<style>
.bar {
display: flex;
gap: 1.5rem;
align-items: center;
height: 3rem;
padding: 0 1rem;
border-bottom: 1px solid var(--rule);
}
.wordmark {
font-size: 1.25rem;
font-weight: 800;
font-stretch: 68%;
letter-spacing: 0.01em;
text-decoration: none;
}
nav {
display: flex;
gap: 1.25rem;
}
nav a {
text-decoration-color: transparent;
}
nav a[aria-current="page"] {
text-decoration-thickness: 2px;
text-decoration-color: var(--red);
}
.tools {
display: flex;
gap: 1.25rem;
margin-left: auto;
}
.plain {
padding: 0.25rem 0;
color: var(--ink-2);
background: none;
border: 0;
transition: color 150ms var(--ease);
}
.plain:hover {
color: var(--ink);
}
@media (max-width: 40rem) {
.bar {
gap: 1rem;
}
.tools {
gap: 0.875rem;
font-size: 0.875rem;
}
nav a:first-child {
display: none;
}
}
</style>
+2
View File
@@ -0,0 +1,2 @@
export const ssr = false;
export const prerender = false;
+209
View File
@@ -0,0 +1,209 @@
<script lang="ts">
import { onMount } from "svelte";
import { api } from "$lib/api/client";
import {
ago,
exact,
lithology,
type Machine,
type Repo,
type Revision,
} from "$lib/lodge";
import { describe } from "$lib/session.svelte";
const CORE_BEDS = 60;
const KEY_LENGTH = 7;
let repos = $state<Repo[] | null>(null);
let machines: string[] = $state([]);
let cores: Record<string, Revision[]> = $state({});
let problem = $state("");
const load = async () => {
try {
const [list, known] = await Promise.all([
api.get<Repo[]>("/repos"),
api.get<Machine[]>("/machines"),
]);
repos = list;
machines = known.map((machine) => machine.name);
await Promise.all(
list.map(async (repo) => {
const revisions = await api.get<Revision[]>(
`/repos/${repo.key}/revisions`
);
cores[repo.key] = revisions.slice(0, CORE_BEDS);
})
);
} catch (error) {
problem = describe(error);
}
};
onMount(() => {
load();
const events = api.events();
events.onmessage = load;
return () => events.close();
});
</script>
<svelte:head>
<title>Repositories · lodge</title>
</svelte:head>
<main>
<h1>Repositories</h1>
{#if problem}
<p class="problem" role="alert">{problem}</p>
{/if}
{#if repos === null && !problem}
<div class="skeleton row-skeleton"></div>
<div class="skeleton row-skeleton"></div>
{:else if repos?.length === 0}
<div class="empty">
<p>
No repository has been logged yet. Connect a machine, then open any git
repository on it: lodge notices the repository on the next checkout, or
right away with <span class="mono">lodge add</span>.
</p>
<a class="act" href="/machines">Connect a machine</a>
</div>
{:else if repos}
<ul>
{#each repos as repo (repo.key)}
<li>
<a href="/r/{repo.key}">
<span class="name">{repo.name}</span>
<span class="facts">
<span class="mono" title={repo.key}
>{repo.key.slice(0, KEY_LENGTH)}</span
>
<span class="stamp">{repo.seq}</span>
{repo.seq === 1 ? "revision" : "revisions"}, last
<time
datetime={new Date(repo.time).toISOString()}
title={exact(repo.time)}
>{ago(repo.time)}</time
>
{#if cores[repo.key]?.[0]}
by {cores[repo.key][0].machine}
{/if}
</span>
<span aria-hidden="true" class="core">
{#each cores[repo.key] ?? [] as rev (rev.seq)}
<i
class={lithology(rev.machine, machines)}
style:flex-grow={Math.min(rev.changes.length, 6)}
></i>
{/each}
</span>
</a>
</li>
{/each}
</ul>
{/if}
</main>
<style>
main {
max-width: 72rem;
padding: 3rem 1.5rem 6rem;
margin: 0 auto;
}
h1 {
margin-bottom: 1.5rem;
font-size: 1rem;
font-weight: 500;
color: var(--ink-2);
}
li a {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.25rem 2rem;
padding: 1.25rem 0 2rem;
text-decoration: none;
border-top: 1px solid var(--ink);
}
li a:hover {
color: inherit;
}
.name {
font-size: clamp(2.25rem, 6vw, 4rem);
font-weight: 700;
font-stretch: 66%;
line-height: 1;
overflow-wrap: anywhere;
transition: color 150ms var(--ease);
}
li a:hover .name {
color: var(--green);
}
.facts {
align-self: end;
font-size: 0.9375rem;
color: var(--ink-2);
text-align: right;
}
.facts .stamp {
font-size: 1.125rem;
color: var(--ink);
}
.core {
display: flex;
grid-column: 1 / -1;
height: 1.75rem;
margin-top: 0.75rem;
overflow: hidden;
border: 1px solid var(--ink);
}
.core i {
flex: 1 0 0.5rem;
max-width: 6rem;
border-right: 1px solid var(--ink);
transition: background-color 150ms var(--ease);
}
li a:hover .core i {
--l: var(--green);
}
.empty {
display: grid;
gap: 1.25rem;
justify-items: start;
max-width: 38rem;
padding-top: 1.25rem;
border-top: 1px solid var(--ink);
}
.row-skeleton {
height: 7rem;
margin-bottom: 1rem;
}
@media (max-width: 40rem) {
main {
padding: 2rem 1rem 4rem;
}
li a {
grid-template-columns: minmax(0, 1fr);
}
.facts {
text-align: left;
}
}
</style>
+264
View File
@@ -0,0 +1,264 @@
@import "tailwindcss";
@import "@fontsource-variable/archivo/wdth.css";
@import "@fontsource-variable/jetbrains-mono/wght.css";
@custom-variant dark (&:is(.dark *));
:root {
--paper: #eef0e6;
--paper-2: #e3e7d8;
--ink: #15211d;
--ink-2: #46544d;
--rule: rgb(21 33 29 / 0.2);
--grid: rgb(36 105 79 / 0.13);
--grid-strong: rgb(36 105 79 / 0.3);
--green: #24694f;
--green-wash: rgb(36 105 79 / 0.12);
--red: #b3341f;
--red-wash: rgb(179 52 31 / 0.11);
--ochre: #c9a24a;
--sans: "Archivo Variable", system-ui, sans-serif;
--mono: "JetBrains Mono Variable", ui-monospace, monospace;
--ease: cubic-bezier(0.16, 1, 0.3, 1);
color-scheme: light;
}
.dark {
--paper: #162026;
--paper-2: #1d2a31;
--ink: #e6eadc;
--ink-2: #a5b2a8;
--rule: rgb(230 234 220 / 0.2);
--grid: rgb(111 199 163 / 0.09);
--grid-strong: rgb(111 199 163 / 0.24);
--green: #74caa6;
--green-wash: rgb(116 202 166 / 0.14);
--red: #f08a74;
--red-wash: rgb(240 138 116 / 0.14);
--ochre: #d9b45e;
color-scheme: dark;
}
html {
background: var(--paper);
color: var(--ink);
font-family: var(--sans);
font-size: 100%;
line-height: 1.5;
scrollbar-color: var(--ink-2) transparent;
scrollbar-width: thin;
-webkit-text-size-adjust: 100%;
}
body {
min-height: 100dvh;
caret-color: var(--red);
}
::selection {
color: var(--paper);
background: var(--green);
}
:focus-visible {
outline: 2px solid var(--red);
outline-offset: 2px;
}
a {
color: inherit;
text-decoration-line: underline;
text-decoration-thickness: 1px;
text-decoration-color: var(--rule);
text-underline-offset: 0.22em;
transition:
text-decoration-color 150ms var(--ease),
color 150ms var(--ease);
}
a:hover {
color: var(--green);
text-decoration-color: currentColor;
}
button {
font: inherit;
cursor: pointer;
}
button:disabled {
cursor: default;
opacity: 0.5;
}
.stamp {
font-weight: 600;
font-stretch: 68%;
font-variant-numeric: tabular-nums;
letter-spacing: 0.02em;
}
.label {
font-size: 0.75rem;
font-weight: 500;
font-stretch: 80%;
color: var(--ink-2);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.mono {
font-family: var(--mono);
font-size: 0.8125em;
font-variant-ligatures: none;
}
.act {
padding: 0.125rem 0;
font-weight: 600;
color: var(--green);
background: none;
border: 0;
border-bottom: 1px solid currentColor;
transition:
color 150ms var(--ease),
background-color 150ms var(--ease);
}
.act:hover:not(:disabled) {
background: var(--green-wash);
}
.act.danger {
color: var(--red);
}
.act.danger:hover:not(:disabled) {
background: var(--red-wash);
}
.act.solid {
padding: 0.625rem 1rem;
color: var(--paper);
background: var(--ink);
border: 0;
}
.act.solid:hover:not(:disabled) {
background: var(--green);
}
.act.solid.danger {
color: var(--paper);
background: var(--red);
}
.field {
width: 100%;
padding: 0.5rem 0;
font: inherit;
color: var(--ink);
background: transparent;
border: 0;
border-bottom: 2px solid var(--ink);
border-radius: 0;
}
.field::placeholder {
color: var(--ink-2);
}
.field:focus-visible {
outline: none;
border-bottom-color: var(--red);
}
.problem {
font-weight: 500;
color: var(--red);
}
.lith {
--l: var(--ink);
position: relative;
background-color: var(--paper);
}
.lith::after {
position: absolute;
inset: 0;
content: "";
background: var(--l);
}
.lith-0::after {
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6' fill='none' stroke='black' stroke-width='1'%3E%3Ccircle cx='3' cy='3' r='0.9' fill='black' stroke='none'/%3E%3C/svg%3E")
repeat;
}
.lith-1::after {
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' fill='none' stroke='black' stroke-width='1'%3E%3Cpath d='M-1 9 9 -1M-1 1 1 -1M7 9 9 7'/%3E%3C/svg%3E")
repeat;
}
.lith-2::after {
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='10' fill='none' stroke='black' stroke-width='1'%3E%3Cpath d='M0 2.5h7M6 7.5h6M0 7.5h1'/%3E%3C/svg%3E")
repeat;
}
.lith-3::after {
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' fill='none' stroke='black' stroke-width='1'%3E%3Cpath d='M0 .5h12M0 4.5h12M.5 0v4M6.5 4v4'/%3E%3C/svg%3E")
repeat;
}
.lith-4::after {
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' fill='none' stroke='black' stroke-width='1'%3E%3Cpath d='M4 2v4M2 4h4'/%3E%3C/svg%3E")
repeat;
}
.lith-5::after {
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='6' fill='none' stroke='black' stroke-width='1'%3E%3Cpath d='M0 3q3 -3 6 0t6 0'/%3E%3C/svg%3E")
repeat;
}
.lith-6::after {
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='6' fill='none' stroke='black' stroke-width='1'%3E%3Cpath d='M0 5l4 -4l4 4'/%3E%3C/svg%3E")
repeat;
}
.lith-web {
background-color: var(--ochre);
}
.lith-web::after,
.lith-gone::after {
display: none;
}
.lith-gone {
outline: 1px dashed var(--rule);
outline-offset: -1px;
}
.skeleton {
color: transparent;
background: var(--paper-2);
}
@keyframes settle {
from {
clip-path: inset(0 0 100% 0);
}
to {
clip-path: inset(0);
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
transition-duration: 1ms !important;
animation-duration: 1ms !important;
}
}
+96
View File
@@ -0,0 +1,96 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { ApiError, api } from "$lib/api/client";
import { describe, session } from "$lib/session.svelte";
let password = $state("");
let busy = $state(false);
let problem = $state("");
const submit = async (event: SubmitEvent) => {
event.preventDefault();
busy = true;
problem = "";
try {
await api.post("/session", { password });
session.signedIn = true;
await goto("/");
} catch (error) {
problem =
error instanceof ApiError && error.status === 401
? "That is not the admin token. Use the ADMIN_TOKEN value from the server's .env file."
: describe(error);
} finally {
busy = false;
}
};
</script>
<svelte:head>
<title>Sign in · lodge</title>
</svelte:head>
<main>
<div aria-hidden="true" class="core lith lith-1"></div>
<form onsubmit={submit}>
<h1>lodge</h1>
<p class="lede">
Agent notes for every repository, at every depth of their history.
</p>
<label class="label" for="password">Admin token</label>
<input
autocomplete="current-password"
class="field mono"
id="password"
required
type="password"
bind:value={password}
>
{#if problem}
<p class="problem" role="alert">{problem}</p>
{/if}
<button class="act solid" disabled={busy} type="submit">
{busy ? "Opening…" : "Open the log"}
</button>
</form>
</main>
<style>
main {
display: grid;
grid-template-columns: clamp(3rem, 12vw, 9rem) minmax(0, 26rem);
gap: clamp(1.5rem, 5vw, 4rem);
min-height: 100dvh;
padding-right: 1.5rem;
}
.core {
--l: var(--green);
border-right: 1px solid var(--rule);
}
form {
display: grid;
gap: 0.5rem;
align-self: center;
justify-items: start;
}
h1 {
font-size: clamp(3.5rem, 10vw, 6rem);
font-weight: 800;
font-stretch: 62%;
line-height: 0.9;
letter-spacing: -0.01em;
}
.lede {
max-width: 22rem;
margin-bottom: 2.5rem;
color: var(--ink-2);
}
.act {
margin-top: 1.5rem;
}
</style>
+307
View File
@@ -0,0 +1,307 @@
<script lang="ts">
import { onMount } from "svelte";
import { api } from "$lib/api/client";
import { ago, type Code, exact, lithology, type Machine } from "$lib/lodge";
import { describe } from "$lib/session.svelte";
const SECOND = 1000;
const MINUTE = 60;
let machines = $state<Machine[] | null>(null);
let problem = $state("");
let code = $state<Code | null>(null);
let now = $state(Date.now());
let copied = $state(false);
let revoking = $state(0);
let busy = $state(false);
const names = $derived(machines?.map((machine) => machine.name) ?? []);
const remaining = $derived(
code ? Math.max(0, Math.round((code.expires_at - now) / SECOND)) : 0
);
const countdown = $derived(
`${Math.floor(remaining / MINUTE)}:${String(remaining % MINUTE).padStart(2, "0")}`
);
const load = async () => {
try {
machines = await api.get<Machine[]>("/machines");
} catch (error) {
problem = describe(error);
}
};
const invite = async () => {
busy = true;
copied = false;
try {
code = await api.post<Code>("/codes");
problem = "";
} catch (error) {
problem = describe(error);
} finally {
busy = false;
}
};
const copy = async () => {
if (code) {
await navigator.clipboard.writeText(code.command);
copied = true;
}
};
const revoke = async (id: number) => {
busy = true;
try {
await api.del(`/machines/${id}`);
revoking = 0;
await load();
} catch (error) {
problem = describe(error);
} finally {
busy = false;
}
};
onMount(() => {
load();
const timer = setInterval(() => {
now = Date.now();
if (code && remaining === 0) {
code = null;
load();
}
}, SECOND);
const events = api.events();
events.onmessage = load;
return () => {
clearInterval(timer);
events.close();
};
});
</script>
<svelte:head>
<title>Machines · lodge</title>
</svelte:head>
<main>
<h1>Machines</h1>
<p class="lede">
The legend of the log. Each machine owns a hatch, and every bed it writes is
drawn with it.
</p>
{#if problem}
<p class="problem" role="alert">{problem}</p>
{/if}
<section class="invite">
{#if code && remaining > 0}
<p>
Run this on the new machine. It installs the client, signs in and starts
the daemon. The link works once and expires in
<span class="stamp">{countdown}</span>.
</p>
<pre class="mono command">{code.command}</pre>
<div class="row">
<button class="act solid" onclick={copy} type="button">
{copied ? "Copied" : "Copy command"}
</button>
<button class="act" disabled={busy} onclick={invite} type="button">
New link
</button>
</div>
{:else}
<button class="act solid" disabled={busy} onclick={invite} type="button">
Connect a machine
</button>
{/if}
</section>
{#if machines === null && !problem}
<div class="skeleton row-skeleton"></div>
{:else if machines?.length === 0}
<p class="empty">
No machine is connected yet. The command above works on macOS and Linux,
on Intel and ARM.
</p>
{:else if machines}
<ul>
{#each machines as machine (machine.id)}
<li>
<span
aria-hidden="true"
class="swatch {lithology(machine.name, names)}"
></span>
<span class="name stamp">{machine.name}</span>
<span class="facts">
<span class="stamp">{machine.revisions}</span>
{machine.revisions === 1 ? "revision" : "revisions"}, last push
{ago(machine.last_push)}. Connected
<time
datetime={new Date(machine.created_at).toISOString()}
title={exact(machine.created_at)}
>{ago(machine.created_at)}</time
>.
</span>
<span class="action">
{#if revoking === machine.id}
<span class="confirm">
Its token stops working at once; files on it stay.
<button
class="act danger"
disabled={busy}
onclick={() => revoke(machine.id)}
type="button"
>
Revoke {machine.name}
</button>
<button
class="act"
onclick={() => {
revoking = 0;
}}
type="button"
>
Keep
</button>
</span>
{:else}
<button
class="act danger"
onclick={() => {
revoking = machine.id;
}}
type="button"
>
Revoke
</button>
{/if}
</span>
</li>
{/each}
<li>
<span aria-hidden="true" class="swatch lith lith-web"></span>
<span class="name stamp">web</span>
<span class="facts">Versions restored from this page.</span>
</li>
</ul>
{/if}
</main>
<style>
main {
max-width: 72rem;
padding: 3rem 1.5rem 6rem;
margin: 0 auto;
}
h1 {
font-size: clamp(2.25rem, 6vw, 4rem);
font-weight: 700;
font-stretch: 66%;
line-height: 1;
}
.lede {
max-width: 34rem;
margin-top: 0.75rem;
color: var(--ink-2);
}
.invite {
display: grid;
gap: 1rem;
justify-items: start;
max-width: 46rem;
margin: 2.5rem 0 3.5rem;
}
.command {
max-width: 100%;
padding: 1rem 1.25rem;
overflow-x: auto;
user-select: all;
background: var(--paper-2);
border-left: 1px solid var(--ink);
}
.row {
display: flex;
gap: 1.5rem;
align-items: baseline;
}
li {
display: grid;
grid-template-columns: 6rem minmax(8rem, 14rem) minmax(0, 1fr) auto;
gap: 0.25rem 1.5rem;
align-items: center;
padding: 1rem 0;
border-top: 1px solid var(--ink);
}
.swatch {
height: 3rem;
border: 1px solid var(--ink);
}
.name {
font-size: 1.75rem;
overflow-wrap: anywhere;
}
.facts {
font-size: 0.9375rem;
color: var(--ink-2);
}
.facts .stamp {
color: var(--ink);
}
.confirm {
display: flex;
flex-wrap: wrap;
gap: 0.25rem 1rem;
justify-content: flex-end;
max-width: 22rem;
font-size: 0.875rem;
}
.empty {
max-width: 34rem;
color: var(--ink-2);
}
.row-skeleton {
height: 5rem;
}
@media (max-width: 48rem) {
main {
padding: 2rem 1rem 4rem;
}
li {
grid-template-columns: 4rem minmax(0, 1fr);
}
.swatch {
grid-row: span 3;
height: 100%;
min-height: 4rem;
}
.facts,
.action {
grid-column: 2;
}
.confirm {
justify-content: flex-start;
}
}
</style>
+815
View File
@@ -0,0 +1,815 @@
<script lang="ts">
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { page } from "$app/state";
import { api } from "$lib/api/client";
import Note from "$lib/components/Note.svelte";
import StripLog from "$lib/components/StripLog.svelte";
import {
ago,
blob,
changeKind,
exact,
fileId,
type Machine,
type Repo,
type RepoState,
type Revision,
SHARED,
shortSha,
spaceName,
text,
} from "$lib/lodge";
import { describe } from "$lib/session.svelte";
interface Opened {
before: string | null;
now: string | null;
}
const KEY_LENGTH = 12;
const TRACK_TICKS = 100;
const key = $derived(page.params.key ?? "");
const query = $derived(page.url.searchParams);
const space = $derived(query.get("space") ?? "");
const path = $derived(query.get("path") ?? "");
const requestedView = $derived(query.get("view") ?? "read");
let revisions: Revision[] = $state([]);
let machines: string[] = $state([]);
let name = $state("");
let loaded = $state(false);
let problem = $state("");
let fresh = $state(0);
let repoState = $state<RepoState | null>(null);
let opened = $state<Opened | null>(null);
let confirming = $state("");
let typedName = $state("");
let busy = $state(false);
const states = new Map<number, Promise<RepoState>>();
const latest = $derived(revisions[0]?.seq ?? 0);
const at = $derived(Number(query.get("at")) || latest);
const bed = $derived(revisions.find((rev) => rev.seq === at));
const markdown = $derived(path.toLowerCase().endsWith(".md"));
const view = $derived(
!markdown && requestedView === "read" ? "source" : requestedView
);
const history = $derived.by(() => {
const byFile = new Map<string, Revision[]>();
for (const rev of revisions) {
for (const change of rev.changes) {
const id = fileId(change.space, change.path);
byFile.set(id, [...(byFile.get(id) ?? []), rev]);
}
}
return byFile;
});
const fileHistory = $derived(history.get(fileId(space, path)) ?? []);
const touched = $derived(
path ? new Set(fileHistory.map((rev) => rev.seq)) : null
);
const entry = $derived(repoState?.spaces[space]?.[path]);
const written = $derived(fileHistory.find((rev) => rev.seq <= at));
const writtenChange = $derived(
written?.changes.find(
(change) => change.space === space && change.path === path
)
);
const surfaceHash = $derived(
fileHistory[0]?.changes.find(
(change) => change.space === space && change.path === path
)?.hash ?? ""
);
const restorable = $derived(Boolean(entry) && entry?.hash !== surfaceHash);
const spaces = $derived(
Object.keys(repoState?.spaces ?? {}).sort((a, b) => {
if (a === SHARED || b === SHARED) {
return a === SHARED ? -1 : 1;
}
return a.localeCompare(b);
})
);
const href = (patch: Record<string, string | number>) => {
const next = new URLSearchParams(query);
for (const [param, value] of Object.entries(patch)) {
if (value === "" || (param === "at" && value === latest)) {
next.delete(param);
} else {
next.set(param, String(value));
}
}
const search = next.toString();
return search ? `?${search}` : page.url.pathname;
};
const select = (seq: number, scrubbing: boolean) => {
confirming = "";
goto(href({ at: seq }), {
keepFocus: true,
noScroll: true,
replaceState: scrubbing,
});
};
const ticks = (space_: string, path_: string) => {
const revs = history.get(fileId(space_, path_)) ?? [];
const oldest = revisions.at(-1)?.seq ?? 0;
const span = Math.max(1, latest - oldest);
return revs.map((rev) => ({
above: rev.seq > at,
left: ((rev.seq - oldest) / span) * TRACK_TICKS,
}));
};
const load = async () => {
try {
const [revs, repos, known] = await Promise.all([
api.get<Revision[]>(`/repos/${key}/revisions`),
api.get<Repo[]>("/repos"),
api.get<Machine[]>("/machines"),
]);
revisions = revs;
name = repos.find((repo) => repo.key === key)?.name ?? "";
machines = known.map((machine) => machine.name);
problem = "";
} catch (error) {
problem = describe(error);
} finally {
loaded = true;
}
};
const stateAt = (seq: number) => {
let pending = states.get(seq);
if (!pending) {
pending = api.get<RepoState>(`/repos/${key}/state?at=${seq}`);
states.set(seq, pending);
}
return pending;
};
const readText = async (hash: string | undefined) =>
hash ? text(await blob(hash)) : "";
$effect(() => {
if (!at) {
return;
}
let stale = false;
stateAt(at)
.then((value) => {
if (!stale) {
repoState = value;
}
})
.catch((error) => {
states.delete(at);
problem = describe(error);
});
return () => {
stale = true;
};
});
$effect(() => {
const hash = entry?.hash;
const base = writtenChange?.base;
if (!hash) {
opened = null;
return;
}
let stale = false;
Promise.all([readText(hash), readText(base)])
.then(([now, before]) => {
if (!stale) {
opened = { before, now };
}
})
.catch((error) => {
problem = describe(error);
});
return () => {
stale = true;
};
});
const restore = async () => {
if (!(entry && revisions[0])) {
return;
}
busy = true;
try {
await api.post(`/repos/${key}/revisions`, {
changes: [{ base: surfaceHash, hash: entry.hash, path, space }],
main_branch: revisions[0].main_branch,
main_sha: revisions[0].main_sha,
name,
});
confirming = "";
await load();
await goto(href({ at: "" }), { noScroll: true });
} catch (error) {
problem = describe(error);
} finally {
busy = false;
}
};
const remove = async () => {
busy = true;
try {
await api.del(`/repos/${key}`);
await goto("/");
} catch (error) {
problem = describe(error);
busy = false;
}
};
onMount(() => {
load();
const events = api.events();
events.onmessage = async (message) => {
const event = JSON.parse(message.data) as { repo: string; seq: number };
if (event.repo === key && event.seq) {
await load();
fresh = event.seq;
}
};
return () => events.close();
});
</script>
<svelte:head>
<title>
{path ? `${path.split("/").at(-1)} · ` : ""}{name || "repository"}
· lodge
</title>
</svelte:head>
{#snippet slashed(value: string)}
{#each value.split("/") as part, index (index)}
{index ? "/" : ""}<wbr>{part}
{/each}
{/snippet}
<div class="sheet" class:reading={Boolean(path)}>
<aside class="log">
<dl class="titleblock">
<div class="wide">
<dt class="label">Repository</dt>
<dd class="name stamp">{name || "…"}</dd>
</div>
<div>
<dt class="label">Depth</dt>
<dd class="stamp">{at} / {latest}</dd>
</div>
<div>
<dt class="label">Root</dt>
<dd class="mono" title={key}>{key.slice(0, KEY_LENGTH)}</dd>
</div>
</dl>
{#if loaded}
<StripLog
{at}
{fresh}
{machines}
onselect={select}
{revisions}
{touched}
/>
{:else}
<div class="skeleton column-skeleton"></div>
{/if}
</aside>
<section aria-label="Files at this depth" class="files">
{#if problem}
<p class="problem" role="alert">{problem}</p>
{/if}
{#if bed}
<div class="bedinfo">
<h2 class="stamp">
Depth {bed.seq}{bed.seq === latest ? ", the surface" : ""}
</h2>
<p>
<strong>{bed.machine}</strong>
<time
datetime={new Date(bed.time).toISOString()}
title={exact(bed.time)}
>{ago(bed.time)}</time
>
on {bed.main_branch || "detached"}
<span class="mono">{shortSha(bed.main_sha)}</span>
</p>
<ul>
{#each bed.changes as change (fileId(change.space, change.path))}
<li class={changeKind(change)}>
<span class="kind">{changeKind(change)}</span>
{#if change.hash}
<a
href={href({ path: change.path, space: change.space, view: "changes" })}
>{@render slashed(change.path)}</a
>
{:else}
<span>{@render slashed(change.path)}</span>
{/if}
</li>
{/each}
</ul>
{#if bed.seq !== latest}
<a class="surface" href={href({ at: "" })}>Back to the surface</a>
{/if}
</div>
{/if}
{#each spaces as group (group)}
<h3 class="label space">
{group === SHARED ? "Shared notes" : `Branch ${spaceName(group)}`}
</h3>
<ul class="rows">
{#each Object.entries(repoState?.spaces[group] ?? {}).sort( ([a], [b]) => a.localeCompare(b) ) as [ file, info ] (file)}
<li>
<a
aria-current={group === space && file === path ? "true" : undefined}
href={href({ path: file, space: group, view: "" })}
>
<span class="file">
<span class="dir"
>{@render slashed(file.slice(0, file.lastIndexOf("/") + 1))}</span
>{file.slice(file.lastIndexOf("/") + 1)}
</span>
<span aria-hidden="true" class="track">
{#each ticks(group, file) as tick (tick.left)}
<i style:left="{tick.left}%" class:above={tick.above}></i>
{/each}
</span>
<span class="when">{ago(info.time)}</span>
</a>
</li>
{/each}
</ul>
{:else}
{#if loaded && !problem}
<p class="empty">
{latest
? "Nothing had been written yet at this depth. Drag the column up towards the surface."
: "This repository has no revisions yet. Write a file under docs/agents on any connected machine and it lands here within seconds."}
</p>
{/if}
{/each}
<details class="removal">
<summary>Delete this repository</summary>
<p>
Erases all {latest} revisions from the server. Files on your machines
stay where they are, and a machine that still syncs this repository
starts a new history. Type <strong>{name}</strong> to confirm.
</p>
<input
aria-label="Repository name"
class="field"
placeholder={name}
bind:value={typedName}
>
<button
class="act solid danger"
disabled={busy || typedName !== name || !name}
onclick={remove}
type="button"
>
Delete with all history
</button>
</details>
</section>
<main class="reader">
{#if path}
<a class="back" href={href({ path: "", space: "", view: "" })}>Files</a>
<h1>{path.split("/").at(-1)}</h1>
<p class="meta">
{space === SHARED ? "shared" : `branch ${spaceName(space)}`}
<span class="mono">{path}</span>
{#if written}
written at depth {written.seq} by {written.machine},
<time
datetime={new Date(written.time).toISOString()}
title={exact(written.time)}
>{ago(written.time)}</time
>, on
<span class="together"
>{written.main_branch || "detached"}
<span class="mono">{shortSha(written.main_sha)}</span></span
>
{/if}
</p>
{#if entry}
<div class="controls">
{#each [["read", "Read"], ["changes", "Changes"], ["source", "Source"]] as [ id, title ] (id)}
{#if markdown || id !== "read"}
<a
aria-current={view === id ? "true" : undefined}
href={href({ view: id })}
>{title}</a
>
{/if}
{/each}
{#if restorable}
{#if confirming === "restore"}
<span class="confirm">
Put depth {written?.seq} back on the surface?
<button
class="act"
disabled={busy}
onclick={restore}
type="button"
>
Restore
</button>
<button
class="act danger"
onclick={() => {
confirming = "";
}}
type="button"
>
Keep current
</button>
</span>
{:else}
<button
class="act"
onclick={() => {
confirming = "restore";
}}
type="button"
>
Restore this version
</button>
{/if}
{/if}
</div>
{#if opened === null}
<div class="skeleton text-skeleton"></div>
{:else if opened.now === null}
<p class="empty">
This file is binary, so there is nothing to read here. Fetch it with
<span class="mono">lodge show {at} {path}</span>.
</p>
{:else}
<Note before={opened.before} {markdown} now={opened.now} {view} />
{/if}
{:else if repoState}
<p class="empty">
{path.split("/").at(-1)}
does not exist at depth {at}.
{#if fileHistory.length}
It was last written at depth
<a href={href({ at: fileHistory[0].seq })}>{fileHistory[0].seq}</a>.
{/if}
</p>
{/if}
{:else}
<p class="empty hint">
Pick a file to pull its core from depth {at}. Drag along the column, or
use the arrow keys, to read the notes as they were at any revision.
</p>
{/if}
</main>
</div>
<style>
.sheet {
display: grid;
grid-template-columns: 17.5rem 20rem minmax(0, 1fr);
height: calc(100dvh - 3rem);
}
.log {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
border-right: 1px solid var(--ink);
}
.titleblock {
display: grid;
grid-template-columns: 1fr 1fr;
border-bottom: 1px solid var(--ink);
}
.titleblock div {
padding: 0.5rem 0.75rem;
border-top: 1px solid var(--rule);
}
.titleblock div + div + div {
border-left: 1px solid var(--rule);
}
.titleblock .wide {
grid-column: 1 / -1;
border-top: 0;
}
.name {
font-size: 1.75rem;
line-height: 1.1;
overflow-wrap: anywhere;
}
.column-skeleton {
height: 100%;
}
.files,
.reader {
padding: 1.5rem;
overflow-y: auto;
}
.files {
border-right: 1px solid var(--rule);
}
.bedinfo {
margin-bottom: 2rem;
}
.bedinfo h2 {
font-size: 1.375rem;
}
.bedinfo p,
.meta {
font-size: 0.875rem;
color: var(--ink-2);
}
.bedinfo strong {
color: var(--ink);
}
.bedinfo ul {
margin-top: 0.625rem;
font-size: 0.875rem;
}
.bedinfo li {
display: grid;
grid-template-columns: 4.25rem minmax(0, 1fr);
gap: 0.5rem;
padding: 0.125rem 0;
overflow-wrap: break-word;
}
.kind {
font-size: 0.75rem;
font-weight: 600;
font-stretch: 80%;
line-height: 1.3125rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.added .kind {
color: var(--green);
}
.deleted .kind,
.deleted span + span {
color: var(--red);
}
.deleted span + span {
text-decoration: line-through;
}
.surface {
display: inline-block;
margin-top: 0.75rem;
font-size: 0.875rem;
}
.space {
padding-bottom: 0.25rem;
margin-top: 1.75rem;
border-bottom: 1px solid var(--ink);
}
.rows a {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.125rem 0.75rem;
align-items: baseline;
padding: 0.5rem 0.375rem;
text-decoration: none;
border-bottom: 1px solid var(--rule);
transition: background-color 150ms var(--ease);
}
.rows a:hover {
color: inherit;
background: var(--green-wash);
}
.rows a[aria-current="true"] {
color: var(--paper);
background: var(--ink);
}
.file {
grid-column: 1 / -1;
font-weight: 600;
overflow-wrap: break-word;
}
.dir {
font-weight: 400;
opacity: 0.65;
}
.when {
font-size: 0.8125rem;
white-space: nowrap;
opacity: 0.7;
}
.track {
position: relative;
grid-row: 2;
grid-column: 1 / -1;
align-self: center;
height: 0.4375rem;
border-bottom: 1px solid currentColor;
opacity: 0.85;
}
.track i {
position: absolute;
bottom: 0;
width: 2px;
height: 100%;
background: currentColor;
}
.track i.above {
opacity: 0.3;
}
.empty {
max-width: 34rem;
color: var(--ink-2);
}
.hint {
margin-top: 20vh;
font-size: 1.125rem;
}
.removal {
margin-top: 4rem;
font-size: 0.875rem;
color: var(--ink-2);
}
.removal summary {
cursor: pointer;
}
.removal[open] {
color: var(--ink);
}
.removal p {
margin: 0.75rem 0;
}
.removal .act {
margin-top: 0.75rem;
}
.reader {
padding: 2rem clamp(1.5rem, 4vw, 4rem) 6rem;
}
.back {
display: none;
}
h1 {
font-size: 2.5rem;
font-weight: 700;
font-stretch: 75%;
line-height: 1.05;
overflow-wrap: anywhere;
}
.meta {
margin-top: 0.5rem;
}
.together {
white-space: nowrap;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1.5rem;
align-items: baseline;
padding-bottom: 0.5rem;
margin: 1.5rem 0 2rem;
border-bottom: 1px solid var(--rule);
}
.controls a {
text-decoration-color: transparent;
}
.controls a[aria-current="true"] {
font-weight: 700;
text-decoration-thickness: 2px;
text-decoration-color: var(--red);
}
.controls > :is(button, .confirm) {
margin-left: auto;
}
.confirm {
display: flex;
flex-wrap: wrap;
gap: 0.25rem 1rem;
}
.text-skeleton {
max-width: 70ch;
height: 16rem;
}
@media (max-width: 72rem) {
.sheet {
grid-template-columns: 15rem 17rem minmax(0, 1fr);
}
}
@media (max-width: 56rem) {
.sheet {
grid-template-columns: 4.5rem minmax(0, 1fr);
}
.titleblock {
display: block;
}
.titleblock div:not(.wide) {
display: none;
}
.titleblock .wide {
padding: 0.375rem;
}
.titleblock .label {
display: none;
}
.name {
font-size: 0.8125rem;
}
.files,
.reader {
padding: 1.25rem 1rem 5rem;
}
.files {
border-right: 0;
}
.sheet.reading .files,
.sheet:not(.reading) .reader {
display: none;
}
.back {
display: inline-block;
margin-bottom: 1rem;
font-size: 0.875rem;
}
h1 {
font-size: 1.75rem;
}
}
</style>
+9
View File
@@ -0,0 +1,9 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<title>lodge</title>
<rect fill="#eef0e6" height="32" width="32" />
<rect fill="#24694f" height="7" width="14" x="9" y="3" />
<rect fill="#c9a24a" height="4" width="14" x="9" y="11" />
<rect fill="#33444c" height="9" width="14" x="9" y="16" />
<rect fill="#24694f" height="3" width="14" x="9" y="26" />
<rect fill="#b3341f" height="2" width="24" x="4" y="15" />
</svg>

After

Width:  |  Height:  |  Size: 444 B

+18
View File
@@ -0,0 +1,18 @@
import adapter from "@sveltejs/adapter-static";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
assets: "build",
fallback: "index.html",
pages: "build",
precompress: false,
strict: false,
}),
},
preprocess: vitePreprocess(),
};
export default config;
+20
View File
@@ -0,0 +1,20 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"rewriteRelativeImportExtensions": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}
+20
View File
@@ -0,0 +1,20 @@
import { sveltekit } from "@sveltejs/kit/vite";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
const allowedHosts = process.env.ALLOWED_HOSTS
? process.env.ALLOWED_HOSTS.split(",")
: undefined;
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
server: {
allowedHosts,
proxy: {
"/api": {
changeOrigin: true,
target: process.env.API_PROXY_TARGET ?? "http://localhost:8080",
},
},
},
});