diff --git a/.env.example b/.env.example index 6472abe..a4a5fe3 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,9 @@ AUTH_URL=https://cloud.campbellwireless.net/trips/auth # Auth.js secret — generate with: openssl rand -base64 32 AUTH_SECRET= +# Admin — comma-separated user IDs (from auth provider) that can access /admin +ADMIN_USER_IDS= + # Database # SQLite (default): file:trips.db # Postgres (future): postgresql://user:password@host:5432/trips diff --git a/CLAUDE.md b/CLAUDE.md index e69de29..05baf03 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -0,0 +1,59 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Tech Stack + +- SvelteKit 5 + TypeScript + Bun + Tailwind v4 + better-sqlite3 +- Auth via Synology SSO Server (OIDC) using Auth.js + +## Development Commands + +```bash +bun run dev # Start dev server +bun run build # Production build +bun run check # TypeScript type-check +bun run lint # ESLint (must pass with 0 errors) +bun run format # Prettier formatting +bun run test # Run unit tests (must all pass) +bun run test:watch # Run tests in watch mode +bun run test:coverage # Run tests with coverage report +``` + +## Code Quality + +### Linting and tests must pass before considering any task complete + +After making changes, always verify: +1. `bun run lint` — must exit with 0 errors (warnings are acceptable) +2. `bun run test` — all tests must pass + +### Write unit tests after every major feature + +When adding or significantly modifying server-side business logic (files under `src/lib/server/`), write corresponding unit tests in a `.test.ts` file alongside the module (e.g. `src/lib/server/lodgings.test.ts`). + +Tests use Vitest with an in-memory SQLite database. Use the `setupTestDb()` helper from `src/tests/helpers.ts` to get a fresh DB per test: + +```ts +import { setupTestDb } from '../../tests/helpers.js'; +import type { Database } from './db/types.js'; + +let db: Database; +beforeEach(() => { db = setupTestDb(); }); +afterEach(() => { db.close(); }); +``` + +Test coverage should include the happy path, optional fields, ownership/authorisation checks, and any relationship linking (e.g. guests, travellers). + +### ESLint notes + +- `svelte/require-each-key` is set to `warn` — keyless `{#each}` blocks are acceptable where keys are not meaningful +- `@typescript-eslint/no-unused-expressions` is off — the `value;` reactivity-tracking pattern used in Svelte 5 `$effect` blocks is intentional +- Prefix unused variables/args/catch bindings with `_` to satisfy the no-unused-vars rule +- Use `// eslint-disable-next-line svelte/prefer-svelte-reactivity` for `new URLSearchParams()` used as local temporaries (not reactive state) + +## DB / Testing Architecture + +All server modules import `db` from `$lib/server/db/index.ts`. In tests, `_setDb()` is used to inject an in-memory SQLite instance, bypassing the `$env/dynamic/private` SvelteKit virtual module entirely. Never call `_setDb` in production code. + +SvelteKit virtual modules (`$env/dynamic/private`, `$app/paths`) are stubbed via path aliases in `vitest.config.ts`. diff --git a/bun.lock b/bun.lock index 40734e5..6b39a06 100644 --- a/bun.lock +++ b/bun.lock @@ -9,11 +9,16 @@ "better-sqlite3": "^12.6.2", }, "devDependencies": { + "@eslint/js": "^10.0.1", "@sveltejs/adapter-node": "^5.5.3", "@sveltejs/kit": "^2.50.2", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@tailwindcss/vite": "^4.2.0", "@types/better-sqlite3": "^7.6.13", + "@vitest/coverage-v8": "^4.0.18", + "eslint": "^10.0.0", + "eslint-plugin-svelte": "^3.15.0", + "globals": "^17.3.0", "prettier": "^3.8.1", "prettier-plugin-svelte": "^3.4.1", "prettier-plugin-tailwindcss": "^0.7.2", @@ -21,7 +26,9 @@ "svelte-check": "^4.3.6", "tailwindcss": "^4.2.0", "typescript": "^5.9.3", + "typescript-eslint": "^8.56.0", "vite": "^7.3.1", + "vitest": "^4.0.18", }, }, }, @@ -30,6 +37,16 @@ "@auth/sveltekit": ["@auth/sveltekit@1.11.1", "", { "dependencies": { "@auth/core": "0.41.1", "set-cookie-parser": "^2.7.0" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.3", "@sveltejs/kit": "^1.0.0 || ^2.0.0", "nodemailer": "^7.0.7", "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0-0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-cWNfXcKrNIVtJYOY1tq7H7m03j89Wg7xrTvOJALu18fZdYulzYCPIAdTw8XSEzOp6KyhOGo7tmW7VtzRNtr/8Q=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], @@ -82,6 +99,30 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.23.1", "", { "dependencies": { "@eslint/object-schema": "^3.0.1", "debug": "^4.3.1", "minimatch": "^10.1.1" } }, "sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.5.2", "", { "dependencies": { "@eslint/core": "^1.1.0" } }, "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ=="], + + "@eslint/core": ["@eslint/core@1.1.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw=="], + + "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="], + + "@eslint/object-schema": ["@eslint/object-schema@3.0.1", "", {}, "sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.0", "", { "dependencies": { "@eslint/core": "^1.1.0", "levn": "^0.4.1" } }, "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@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=="], @@ -198,22 +239,76 @@ "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/type-utils": "8.56.0", "@typescript-eslint/utils": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.0", "@typescript-eslint/types": "^8.56.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0" } }, "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0", "@typescript-eslint/utils": "8.56.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.56.0", "", {}, "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.0", "@typescript-eslint/tsconfig-utils": "8.56.0", "@typescript-eslint/types": "8.56.0", "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg=="], + + "@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.18", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.18", "ast-v8-to-istanbul": "^0.3.10", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.18", "vitest": "4.0.18" }, "optionalPeers": ["@vitest/browser"] }, "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg=="], + + "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], + + "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="], + + "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="], + + "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="], + + "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.11", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw=="], + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + "balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "better-sqlite3": ["better-sqlite3@12.6.2", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA=="], @@ -222,8 +317,12 @@ "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], @@ -234,10 +333,18 @@ "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=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "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=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -248,20 +355,58 @@ "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="], + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@10.0.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.0", "@eslint/config-helpers": "^0.5.2", "@eslint/core": "^1.1.0", "@eslint/plugin-kit": "^0.6.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.0", "eslint-visitor-keys": "^5.0.0", "espree": "^11.1.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.1.1", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg=="], + + "eslint-plugin-svelte": ["eslint-plugin-svelte@3.15.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", "globals": "^16.0.0", "known-css-properties": "^0.37.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", "svelte-eslint-parser": "^1.4.0" }, "peerDependencies": { "eslint": "^8.57.1 || ^9.0.0 || ^10.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-QKB7zqfuB8aChOfBTComgDptMf2yxiJx7FE04nneCmtQzgTHvY8UJkuh8J2Rz7KB9FFV9aTHX6r7rdYGvG8T9Q=="], + + "eslint-scope": ["eslint-scope@9.1.0", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.0", "", {}, "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q=="], + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + "espree": ["espree@11.1.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + "esrap": ["esrap@2.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ=="], + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "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=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -270,28 +415,66 @@ "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@17.3.0", "", {}, "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "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-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + "known-css-properties": ["known-css-properties@0.37.0", "", {}, "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="], + + "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.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], @@ -316,12 +499,22 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], + "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + "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=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="], + + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + "minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], @@ -330,10 +523,14 @@ "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="], "oauth4webapi": ["oauth4webapi@3.8.5", "", {}, "sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg=="], @@ -342,20 +539,42 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "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-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="], + + "postcss-safe-parser": ["postcss-safe-parser@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A=="], + + "postcss-scss": ["postcss-scss@4.0.9", "", { "peerDependencies": { "postcss": "^8.4.29" } }, "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A=="], + + "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + "preact": ["preact@10.24.3", "", {}, "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA=="], "preact-render-to-string": ["preact-render-to-string@6.5.11", "", { "peerDependencies": { "preact": ">=10" } }, "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw=="], "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], "prettier-plugin-svelte": ["prettier-plugin-svelte@3.4.1", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg=="], @@ -364,6 +583,8 @@ "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], @@ -382,6 +603,12 @@ "set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], + "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=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], @@ -390,16 +617,24 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "svelte": ["svelte@5.51.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-3+ni7BMjiEQeMCa1fDQzHy2ESAebgQDVOTuE4jlj2/QOAB2grRta8ew80p95miWE+ZmimpL7B3t9SSO4rv0aqQ=="], "svelte-check": ["svelte-check@4.4.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "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" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-gB3FdEPb8tPO3Y7Dzc6d/Pm/KrXAhK+0Fk+LkcysVtupvAh6Y/IrBCEZNupq57oh0hcwlxCUamu/rq7GtvfSEg=="], + "svelte-eslint-parser": ["svelte-eslint-parser@1.4.1", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA=="], + "tailwindcss": ["tailwindcss@4.2.0", "", {}, "sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q=="], "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], @@ -408,28 +643,56 @@ "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript-eslint": ["typescript-eslint@8.56.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.0", "@typescript-eslint/parser": "8.56.0", "@typescript-eslint/typescript-estree": "8.56.0", "@typescript-eslint/utils": "8.56.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], + "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], "@auth/sveltekit/set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], @@ -443,5 +706,25 @@ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "ast-v8-to-istanbul/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "eslint-plugin-svelte/globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], + + "svelte-eslint-parser/eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "svelte-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "svelte-eslint-parser/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], } } diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..fdcebfd --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,41 @@ +import js from '@eslint/js'; +import ts from 'typescript-eslint'; +import svelte from 'eslint-plugin-svelte'; +import globals from 'globals'; + +export default [ + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs['flat/recommended'], + { + languageOptions: { + globals: { ...globals.browser, ...globals.node } + } + }, + { + files: ['**/*.svelte'], + languageOptions: { + parserOptions: { parser: ts.parser } + } + }, + { + rules: { + // The `search;` pattern is intentional for Svelte 5 $effect reactivity tracking + '@typescript-eslint/no-unused-expressions': 'off', + // {@html} is used for inline SVG icons where content is hardcoded and safe + 'svelte/no-at-html-tags': 'off', + // Plain links don't need SvelteKit's resolve() — only programmatic goto() does + 'svelte/no-navigation-without-resolve': 'off', + // Demote to warning so {#each} without key is flagged but doesn't block CI + 'svelte/require-each-key': 'warn', + // Allow intentional unused vars prefixed with _ (catch-block placeholders etc.) + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' } + ] + } + }, + { + ignores: ['.svelte-kit/', 'build/', 'node_modules/'] + } +]; diff --git a/package.json b/package.json index 7a8a5e7..610232c 100644 --- a/package.json +++ b/package.json @@ -9,14 +9,24 @@ "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" + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "eslint src", + "format": "prettier --write .", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@sveltejs/adapter-node": "^5.5.3", "@sveltejs/kit": "^2.50.2", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@tailwindcss/vite": "^4.2.0", "@types/better-sqlite3": "^7.6.13", + "@vitest/coverage-v8": "^4.0.18", + "eslint": "^10.0.0", + "eslint-plugin-svelte": "^3.15.0", + "globals": "^17.3.0", "prettier": "^3.8.1", "prettier-plugin-svelte": "^3.4.1", "prettier-plugin-tailwindcss": "^0.7.2", @@ -24,7 +34,9 @@ "svelte-check": "^4.3.6", "tailwindcss": "^4.2.0", "typescript": "^5.9.3", - "vite": "^7.3.1" + "typescript-eslint": "^8.56.0", + "vite": "^7.3.1", + "vitest": "^4.0.18" }, "dependencies": { "@auth/sveltekit": "^1.11.1", diff --git a/src/lib/components/AddFlightModal.svelte b/src/lib/components/AddFlightModal.svelte index 4ea1cc0..66222b2 100644 --- a/src/lib/components/AddFlightModal.svelte +++ b/src/lib/components/AddFlightModal.svelte @@ -39,9 +39,10 @@ onclose: () => void; people: Person[]; tripTravellerIds: string[]; + parentPlanId?: string; } - let { open, onclose, people = [], tripTravellerIds = [] }: Props = $props(); + let { open, onclose, people = [], tripTravellerIds = [], parentPlanId }: Props = $props(); let confirmationNumber = $state(''); let price = $state(''); @@ -65,9 +66,7 @@ ]); // Search states - let airportQuery = $state(''); let airports = $state([]); - let airlineQuery = $state(''); let airlines = $state([]); let loadingAirports = $state(false); let loadingAirlines = $state(false); @@ -254,6 +253,9 @@ }; }} > + {#if parentPlanId} + + {/if}
diff --git a/src/lib/components/AddLodgingModal.svelte b/src/lib/components/AddLodgingModal.svelte index f8aac8e..1cb29b8 100644 --- a/src/lib/components/AddLodgingModal.svelte +++ b/src/lib/components/AddLodgingModal.svelte @@ -21,9 +21,10 @@ onclose: () => void; people: Person[]; tripTravellerIds: string[]; + parentPlanId?: string; } - let { open, onclose, people = [], tripTravellerIds = [] }: Props = $props(); + let { open, onclose, people = [], tripTravellerIds = [], parentPlanId }: Props = $props(); const TIMEZONES = [ { label: 'UTC (UTC+0)', value: 'UTC' }, @@ -107,6 +108,7 @@ cityDebounce = setTimeout(async () => { loadingCities = true; try { + // eslint-disable-next-line svelte/prefer-svelte-reactivity const params = new URLSearchParams({ q: query.trim() }); if (countryCode) params.set('country_code', countryCode); const res = await fetch(`${base}/api/cities?${params}`); @@ -259,6 +261,9 @@ }; }} > + {#if parentPlanId} + + {/if}
@@ -459,7 +464,14 @@ {#if loadingCities}
- +
@@ -514,7 +526,14 @@ {#if loadingCountries}
- +
@@ -543,8 +562,7 @@ {/if}
- + + import { enhance } from '$app/forms'; + import { base } from '$app/paths'; + import type { Person } from '$lib/server/travellers.js'; + + interface TourOperator { + id: number; + name: string; + website: string | null; + highlight_color: string | null; + } + + interface Props { + open: boolean; + onclose: () => void; + people: Person[]; + tripTravellerIds: string[]; + } + + let { open, onclose, people = [], tripTravellerIds = [] }: Props = $props(); + + const TIMEZONES = [ + { label: 'UTC (UTC+0)', value: 'UTC' }, + { label: 'London (GMT/BST, UTC+0/+1)', value: 'Europe/London' }, + { label: 'Dublin (IST, UTC+0/+1)', value: 'Europe/Dublin' }, + { label: 'Lisbon (WET/WEST, UTC+0/+1)', value: 'Europe/Lisbon' }, + { label: 'Paris / Berlin / Rome (CET, UTC+1/+2)', value: 'Europe/Paris' }, + { label: 'Helsinki / Athens (EET, UTC+2/+3)', value: 'Europe/Helsinki' }, + { label: 'Istanbul (TRT, UTC+3)', value: 'Europe/Istanbul' }, + { label: 'Moscow (MSK, UTC+3)', value: 'Europe/Moscow' }, + { label: 'Dubai (GST, UTC+4)', value: 'Asia/Dubai' }, + { label: 'Karachi (PKT, UTC+5)', value: 'Asia/Karachi' }, + { label: 'Kolkata (IST, UTC+5:30)', value: 'Asia/Kolkata' }, + { label: 'Dhaka (BST, UTC+6)', value: 'Asia/Dhaka' }, + { label: 'Bangkok (ICT, UTC+7)', value: 'Asia/Bangkok' }, + { label: 'Singapore / Kuala Lumpur (SGT/MYT, UTC+8)', value: 'Asia/Singapore' }, + { label: 'Hong Kong (HKT, UTC+8)', value: 'Asia/Hong_Kong' }, + { label: 'Shanghai / Beijing (CST, UTC+8)', value: 'Asia/Shanghai' }, + { label: 'Perth (AWST, UTC+8)', value: 'Australia/Perth' }, + { label: 'Tokyo / Seoul (JST/KST, UTC+9)', value: 'Asia/Tokyo' }, + { label: 'Darwin (ACST, UTC+9:30)', value: 'Australia/Darwin' }, + { label: 'Brisbane (AEST, UTC+10)', value: 'Australia/Brisbane' }, + { label: 'Sydney / Melbourne (AEST/AEDT, UTC+10/+11)', value: 'Australia/Sydney' }, + { label: 'Auckland (NZST/NZDT, UTC+12/+13)', value: 'Pacific/Auckland' }, + { label: 'Fiji (FJT, UTC+12)', value: 'Pacific/Fiji' }, + { label: 'Azores (AZOT/AZOST, UTC-1/0)', value: 'Atlantic/Azores' }, + { label: 'Cape Verde (CVT, UTC-1)', value: 'Atlantic/Cape_Verde' }, + { label: 'Buenos Aires (ART, UTC-3)', value: 'America/Argentina/Buenos_Aires' }, + { label: 'Sao Paulo (BRT/BRST, UTC-3/−2)', value: 'America/Sao_Paulo' }, + { label: 'Halifax (AST/ADT, UTC-4/−3)', value: 'America/Halifax' }, + { label: 'New York / Toronto (EST/EDT, UTC-5/−4)', value: 'America/New_York' }, + { label: 'Chicago / Mexico City (CST/CDT, UTC-6/−5)', value: 'America/Chicago' }, + { label: 'Denver (MST/MDT, UTC-7/−6)', value: 'America/Denver' }, + { label: 'Phoenix (MST, UTC-7)', value: 'America/Phoenix' }, + { label: 'Los Angeles / Vancouver (PST/PDT, UTC-8/−7)', value: 'America/Los_Angeles' }, + { label: 'Anchorage (AKST/AKDT, UTC-9/−8)', value: 'America/Anchorage' }, + { label: 'Honolulu (HST, UTC-10)', value: 'Pacific/Honolulu' } + ]; + + // Operator search + let operatorName = $state(''); + let operatorQuery = $state(''); + let operators = $state([]); + let loadingOperators = $state(false); + let showOperatorDropdown = $state(false); + let operatorDebounce: ReturnType; + // Logo for the selected operator + let operatorLogoUrl = $state(null); + + function logoSlug(name: string): string { + return name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '_') + .replace(/^_|_$/g, ''); + } + + async function checkOperatorLogo(name: string) { + operatorLogoUrl = null; + if (!name) return; + const sq = `${base}/package-tour-logos/${logoSlug(name)}_square.svg`; + const wd = `${base}/package-tour-logos/${logoSlug(name)}_wide.svg`; + try { + const r = await fetch(sq, { method: 'HEAD' }); + if (r.ok) { + operatorLogoUrl = sq; + return; + } + } catch { + /* ignore */ + } + try { + const r = await fetch(wd, { method: 'HEAD' }); + if (r.ok) { + operatorLogoUrl = wd; + return; + } + } catch { + /* ignore */ + } + } + + function searchOperators(query: string) { + clearTimeout(operatorDebounce); + operatorDebounce = setTimeout(async () => { + loadingOperators = true; + try { + const params = query.trim() ? `?q=${encodeURIComponent(query.trim())}` : ''; + const res = await fetch(`${base}/api/tour-operators${params}`); + operators = await res.json(); + } finally { + loadingOperators = false; + } + }, 200); + } + + function onOperatorInput(value: string) { + operatorName = value; + operatorQuery = value; + showOperatorDropdown = true; + searchOperators(value); + } + + function selectOperator(op: TourOperator) { + operatorName = op.name; + operatorQuery = op.name; + showOperatorDropdown = false; + checkOperatorLogo(op.name); + } + + // Core form state + let status = $state<'idea' | 'tentative' | 'confirmed'>('idea'); + let confirmationNumber = $state(''); + let startDate = $state(''); + let startTime = $state(''); + let startTimezone = $state(''); + let endDate = $state(''); + let endTime = $state(''); + let endTimezone = $state(''); + let price = $state(''); + let currency = $state('USD'); + let selectedTravellers = $state([]); + + function toggleTraveller(personId: string) { + if (selectedTravellers.includes(personId)) { + selectedTravellers = selectedTravellers.filter((id) => id !== personId); + } else { + selectedTravellers = [...selectedTravellers, personId]; + } + } + + function reset() { + operatorName = ''; + operatorQuery = ''; + operators = []; + showOperatorDropdown = false; + operatorLogoUrl = null; + status = 'idea'; + confirmationNumber = ''; + startDate = ''; + startTime = ''; + startTimezone = ''; + endDate = ''; + endTime = ''; + endTimezone = ''; + price = ''; + currency = 'USD'; + selectedTravellers = []; + } + + function handleClose() { + reset(); + onclose(); + } + + // Load operators when modal opens + $effect(() => { + if (open && operators.length === 0) { + searchOperators(''); + } + }); + + let canSubmit = $derived(operatorName.trim().length > 0); + + +{#if open} + +
e.key === 'Escape' && handleClose()} + >
+ + + +{/if} diff --git a/src/lib/components/AdminNavMenu.svelte b/src/lib/components/AdminNavMenu.svelte new file mode 100644 index 0000000..48b0100 --- /dev/null +++ b/src/lib/components/AdminNavMenu.svelte @@ -0,0 +1,98 @@ + + +
diff --git a/src/lib/components/EditFlightModal.svelte b/src/lib/components/EditFlightModal.svelte index b8aebe1..b988709 100644 --- a/src/lib/components/EditFlightModal.svelte +++ b/src/lib/components/EditFlightModal.svelte @@ -125,9 +125,7 @@ }); // Search states - let airportQuery = $state(''); let airports = $state([]); - let airlineQuery = $state(''); let airlines = $state([]); let loadingAirports = $state(false); let loadingAirlines = $state(false); diff --git a/src/lib/components/EditLodgingModal.svelte b/src/lib/components/EditLodgingModal.svelte index fd7e0d7..6e242e9 100644 --- a/src/lib/components/EditLodgingModal.svelte +++ b/src/lib/components/EditLodgingModal.svelte @@ -110,6 +110,7 @@ cityDebounce = setTimeout(async () => { loadingCities = true; try { + // eslint-disable-next-line svelte/prefer-svelte-reactivity const params = new URLSearchParams({ q: query.trim() }); if (countryCode) params.set('country_code', countryCode); const res = await fetch(`${base}/api/cities?${params}`); @@ -369,8 +370,7 @@
Check-out
- Date
- Time - +
@@ -519,7 +525,14 @@ {#if loadingCountries}
- +
diff --git a/src/lib/components/EditPackageTourModal.svelte b/src/lib/components/EditPackageTourModal.svelte new file mode 100644 index 0000000..82ee3ac --- /dev/null +++ b/src/lib/components/EditPackageTourModal.svelte @@ -0,0 +1,526 @@ + + +{#if open && tour} + +
e.key === 'Escape' && handleClose()} + >
+ + + +{/if} diff --git a/src/lib/components/PackageTourCard.svelte b/src/lib/components/PackageTourCard.svelte new file mode 100644 index 0000000..9ce9c98 --- /dev/null +++ b/src/lib/components/PackageTourCard.svelte @@ -0,0 +1,393 @@ + + +
+ +
+
+
+ + {#if logoUrl} + {tour.operator_name} + {:else} + + + + + + + {/if} +
+
+

{tour.operator_name}

+ + {statusConfig[plan.status].label} + +
+
+
+
+ +
+ {#if onEdit} + + {/if} + {#if onDelete} + + {/if} +
+
+ + + {#if hasStart || hasEnd} +
+
+

Start

+ {#if hasStart} +

+ {#if tour.start_date} + {formatDate(tour.start_date)}{#if tour.start_time} + · {formatTime(tour.start_time)}{/if} + {:else} + {formatTime(tour.start_time)} + {/if} +

+ {#if tour.start_timezone} +

({tzLabel(tour.start_timezone)})

+ {/if} + {:else} +

+ {/if} +
+
+

End

+ {#if hasEnd} +

+ {#if tour.end_date} + {formatDate(tour.end_date)}{#if tour.end_time} + · {formatTime(tour.end_time)}{/if} + {:else} + {formatTime(tour.end_time)} + {/if} +

+ {#if tour.end_timezone} +

({tzLabel(tour.end_timezone)})

+ {/if} + {:else} +

+ {/if} +
+
+ {/if} + + + {#if tour.confirmation_number || (tour.price != null && tour.price > 0)} +
+ {#if tour.confirmation_number} + Confirmation: {tour.confirmation_number} + {/if} + {#if tour.confirmation_number && tour.price != null && tour.price > 0} + · + {/if} + {#if tour.price != null && tour.price > 0} + {tour.currency} {tour.price.toFixed(2)} + {/if} +
+ {/if} + + + {#if tour.childPlans.length > 0} +
+

Includes

+
+ {#each visibleChildren as child} + {@const config = childTypeConfig[child.type] ?? { label: child.type, icon: '' }} + + {@html config.icon} + {child.title} + + + {/each} + {#if extraChildren > 0} + + +{extraChildren} more + + {/if} +
+
+ {/if} + + + {#if onAddFlight || onAddLodging} +
+ + {#if showAddMenu} +
(showAddMenu = false)} + onkeydown={(e) => e.key === 'Escape' && (showAddMenu = false)} + >
+
+ {#if onAddFlight} + + {/if} + {#if onAddLodging} + + {/if} +
+ {/if} +
+ {/if} +
diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index 3dba44f..1ae96c0 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -4,8 +4,10 @@ interface Props { userName?: string | null; + isAdmin?: boolean; + isAdminRoute?: boolean; } - let { userName }: Props = $props(); + let { userName, isAdmin = false, isAdminRoute = false }: Props = $props(); const initial = $derived(userName ? userName.charAt(0).toUpperCase() : '?'); let appSwitcherOpen = $state(false); @@ -64,8 +66,47 @@ {/if}
- -
+ +
+ {#if isAdmin} + + {#if isAdminRoute} + + + + + {:else} + + + + + {/if} + + {/if} +
+ + + {#if adding || editing} +
+

+ {editing ? 'Edit airline' : 'Add airline'} +

+ {#if error} +

{error}

+ {/if} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ {/if} + + +
+ {#if loading} +

Loading...

+ {:else if items.length === 0} +

No airlines found.

+ {:else} + + + + + + + + + + + {#each items as item (item.id)} + + + + + + + {/each} + +
IATANameCountryActions
+ {#if item.iata_code} + {item.iata_code} + {:else} + + {/if} + {item.name}{item.country ?? '—'} + + · + +
+ {/if} +
+
+
diff --git a/src/routes/(protected)/admin/airports/+page.svelte b/src/routes/(protected)/admin/airports/+page.svelte new file mode 100644 index 0000000..32763a5 --- /dev/null +++ b/src/routes/(protected)/admin/airports/+page.svelte @@ -0,0 +1,342 @@ + + + + Airports — Admin — Trips + + +
+
+

Airports

+

Manage the list of airports used for flight planning.

+
+ +
+ +
+ + +
+ + + {#if adding || editing} +
+

+ {editing ? 'Edit airport' : 'Add airport'} +

+ {#if error} +

{error}

+ {/if} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ {/if} + + +
+ {#if loading} +

Loading...

+ {:else if items.length === 0} +

No airports found.

+ {:else} + + + + + + + + + + + + {#each items as item (item.id)} + + + + + + + + {/each} + +
IATANameCityCountryActions
+ {#if item.iata_code} + {item.iata_code} + {:else} + + {/if} + {item.name}{item.city ?? '—'}{item.country} + + · + +
+ {/if} +
+
+
diff --git a/src/routes/(protected)/admin/api/airlines/+server.ts b/src/routes/(protected)/admin/api/airlines/+server.ts new file mode 100644 index 0000000..dfaef7a --- /dev/null +++ b/src/routes/(protected)/admin/api/airlines/+server.ts @@ -0,0 +1,63 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { requireAdmin } from '$lib/server/admin/auth.js'; +import * as data from '$lib/server/admin/data.js'; + +export const GET: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const q = event.url.searchParams.get('q') ?? undefined; + return json(data.listAirlines(q)); +}; + +export const POST: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const body = await event.request.json(); + const { name, country, country_code, iata_code, icao_code } = body as { + name?: string; + country?: string | null; + country_code?: string | null; + iata_code?: string | null; + icao_code?: string | null; + }; + if (!name?.trim()) return json({ error: 'name required' }, { status: 400 }); + return json( + data.createAirline( + name, + country ?? null, + country_code ?? null, + iata_code ?? null, + icao_code ?? null + ) + ); +}; + +export const PATCH: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const body = await event.request.json(); + const { id, name, country, country_code, iata_code, icao_code } = body as { + id?: number; + name?: string; + country?: string | null; + country_code?: string | null; + iata_code?: string | null; + icao_code?: string | null; + }; + if (id == null || !name?.trim()) return json({ error: 'id and name required' }, { status: 400 }); + data.updateAirline( + id, + name, + country ?? null, + country_code ?? null, + iata_code ?? null, + icao_code ?? null + ); + return json({ ok: true }); +}; + +export const DELETE: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const id = parseInt(event.url.searchParams.get('id') ?? ''); + if (isNaN(id)) return json({ error: 'id required' }, { status: 400 }); + data.deleteAirline(id); + return json({ ok: true }); +}; diff --git a/src/routes/(protected)/admin/api/airports/+server.ts b/src/routes/(protected)/admin/api/airports/+server.ts new file mode 100644 index 0000000..a98347e --- /dev/null +++ b/src/routes/(protected)/admin/api/airports/+server.ts @@ -0,0 +1,95 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { requireAdmin } from '$lib/server/admin/auth.js'; +import * as data from '$lib/server/admin/data.js'; + +export const GET: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const q = event.url.searchParams.get('q') ?? undefined; + return json(data.listAirports(q)); +}; + +export const POST: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const body = await event.request.json(); + const { name, country, country_code, iata_code, icao_code, city, latitude, longitude, timezone } = + body as { + name?: string; + country?: string; + country_code?: string; + iata_code?: string | null; + icao_code?: string | null; + city?: string | null; + latitude?: number | null; + longitude?: number | null; + timezone?: string | null; + }; + if (!name?.trim() || !country?.trim() || !country_code?.trim()) { + return json({ error: 'name, country and country_code required' }, { status: 400 }); + } + return json( + data.createAirport( + name, + country, + country_code, + iata_code ?? null, + icao_code ?? null, + city ?? null, + latitude ?? null, + longitude ?? null, + timezone ?? null + ) + ); +}; + +export const PATCH: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const body = await event.request.json(); + const { + id, + name, + country, + country_code, + iata_code, + icao_code, + city, + latitude, + longitude, + timezone + } = body as { + id?: number; + name?: string; + country?: string; + country_code?: string; + iata_code?: string | null; + icao_code?: string | null; + city?: string | null; + latitude?: number | null; + longitude?: number | null; + timezone?: string | null; + }; + if (id == null || !name?.trim() || !country?.trim() || !country_code?.trim()) { + return json({ error: 'id, name, country and country_code required' }, { status: 400 }); + } + data.updateAirport( + id, + name, + country, + country_code, + iata_code ?? null, + icao_code ?? null, + city ?? null, + latitude ?? null, + longitude ?? null, + timezone ?? null + ); + return json({ ok: true }); +}; + +export const DELETE: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const id = parseInt(event.url.searchParams.get('id') ?? ''); + if (isNaN(id)) return json({ error: 'id required' }, { status: 400 }); + data.deleteAirport(id); + return json({ ok: true }); +}; diff --git a/src/routes/(protected)/admin/api/cities/+server.ts b/src/routes/(protected)/admin/api/cities/+server.ts new file mode 100644 index 0000000..ccf656c --- /dev/null +++ b/src/routes/(protected)/admin/api/cities/+server.ts @@ -0,0 +1,51 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { requireAdmin } from '$lib/server/admin/auth.js'; +import * as data from '$lib/server/admin/data.js'; + +export const GET: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const q = event.url.searchParams.get('q') ?? undefined; + const countryCode = event.url.searchParams.get('country_code') ?? undefined; + return json(data.listCities(q, countryCode)); +}; + +export const POST: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const body = await event.request.json(); + const { name, country, country_code, population } = body as { + name?: string; + country?: string; + country_code?: string; + population?: number | null; + }; + if (!name?.trim() || !country?.trim() || !country_code?.trim()) { + return json({ error: 'name, country and country_code required' }, { status: 400 }); + } + return json(data.createCity(name, country, country_code, population ?? null)); +}; + +export const PATCH: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const body = await event.request.json(); + const { id, name, country, country_code, population } = body as { + id?: number; + name?: string; + country?: string; + country_code?: string; + population?: number | null; + }; + if (id == null || !name?.trim() || !country?.trim() || !country_code?.trim()) { + return json({ error: 'id, name, country and country_code required' }, { status: 400 }); + } + data.updateCity(id, name, country, country_code, population ?? null); + return json({ ok: true }); +}; + +export const DELETE: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const id = parseInt(event.url.searchParams.get('id') ?? ''); + if (isNaN(id)) return json({ error: 'id required' }, { status: 400 }); + data.deleteCity(id); + return json({ ok: true }); +}; diff --git a/src/routes/(protected)/admin/api/countries/+server.ts b/src/routes/(protected)/admin/api/countries/+server.ts new file mode 100644 index 0000000..80f25ba --- /dev/null +++ b/src/routes/(protected)/admin/api/countries/+server.ts @@ -0,0 +1,39 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { requireAdmin } from '$lib/server/admin/auth.js'; +import * as data from '$lib/server/admin/data.js'; + +export const GET: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const q = event.url.searchParams.get('q') ?? undefined; + return json(data.listCountries(q)); +}; + +export const POST: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const body = await event.request.json(); + const { name, country_code } = body as { name?: string; country_code?: string }; + if (!name?.trim() || !country_code?.trim()) { + return json({ error: 'name and country_code required' }, { status: 400 }); + } + return json(data.createCountry(name, country_code)); +}; + +export const PATCH: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const body = await event.request.json(); + const { id, name, country_code } = body as { id?: number; name?: string; country_code?: string }; + if (id == null || !name?.trim() || !country_code?.trim()) { + return json({ error: 'id, name and country_code required' }, { status: 400 }); + } + data.updateCountry(id, name, country_code); + return json({ ok: true }); +}; + +export const DELETE: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const id = parseInt(event.url.searchParams.get('id') ?? ''); + if (isNaN(id)) return json({ error: 'id required' }, { status: 400 }); + data.deleteCountry(id); + return json({ ok: true }); +}; diff --git a/src/routes/(protected)/admin/api/tour-operators/+server.ts b/src/routes/(protected)/admin/api/tour-operators/+server.ts new file mode 100644 index 0000000..96f1f89 --- /dev/null +++ b/src/routes/(protected)/admin/api/tour-operators/+server.ts @@ -0,0 +1,48 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { requireAdmin } from '$lib/server/admin/auth.js'; +import * as data from '$lib/server/admin/data.js'; + +export const GET: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const q = event.url.searchParams.get('q') ?? undefined; + return json(data.listTourOperators(q)); +}; + +export const POST: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const body = await event.request.json(); + const { name, website, highlight_color } = body as { + name?: string; + website?: string | null; + highlight_color?: string | null; + }; + if (!name?.trim()) { + return json({ error: 'name is required' }, { status: 400 }); + } + return json(data.createTourOperator(name, website, highlight_color)); +}; + +export const PATCH: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const body = await event.request.json(); + const { id, name, website, highlight_color } = body as { + id?: number; + name?: string; + website?: string | null; + highlight_color?: string | null; + }; + if (id == null || !name?.trim()) { + return json({ error: 'id and name are required' }, { status: 400 }); + } + data.updateTourOperator(id, name, website, highlight_color); + return json({ ok: true }); +}; + +export const DELETE: RequestHandler = async (event) => { + requireAdmin((await event.locals.auth())?.user?.id); + const id = parseInt(event.url.searchParams.get('id') ?? ''); + if (isNaN(id)) return json({ error: 'id required' }, { status: 400 }); + data.deleteTourOperator(id); + return json({ ok: true }); +}; diff --git a/src/routes/(protected)/admin/cities/+page.svelte b/src/routes/(protected)/admin/cities/+page.svelte new file mode 100644 index 0000000..1c3157a --- /dev/null +++ b/src/routes/(protected)/admin/cities/+page.svelte @@ -0,0 +1,317 @@ + + + + Cities — Admin — Trips + + +
+
+

Cities

+

Manage the list of cities used across the app.

+
+ +
+ +
+ + +
+ + + {#if adding || editing} +
+

+ {editing ? 'Edit city' : 'Add city'} +

+ {#if error} +

{error}

+ {/if} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ {/if} + + +
+ {#if loading} +

Loading...

+ {:else if items.length === 0} +

No cities found.

+ {:else} + + + + + + + + + + + + {#each items as item (item.id)} + + + + + + + + {/each} + +
NameCountryCodePopulationActions
{item.name}{item.country} + {item.country_code} + + {item.population != null ? item.population.toLocaleString() : '—'} + + + · + +
+ {/if} +
+
+
diff --git a/src/routes/(protected)/admin/countries/+page.svelte b/src/routes/(protected)/admin/countries/+page.svelte new file mode 100644 index 0000000..1811a2d --- /dev/null +++ b/src/routes/(protected)/admin/countries/+page.svelte @@ -0,0 +1,264 @@ + + + + Countries — Admin — Trips + + +
+
+

Countries

+

Manage the list of countries used across the app.

+
+ +
+ +
+ + +
+ + + {#if adding || editing} +
+

+ {editing ? 'Edit country' : 'Add country'} +

+ {#if error} +

{error}

+ {/if} +
+
+ + +
+
+ + +
+
+ + +
+
+
+ {/if} + + +
+ {#if loading} +

Loading...

+ {:else if items.length === 0} +

No countries found.

+ {:else} + + + + + + + + + + {#each items as item (item.id)} + + + + + + {/each} + +
NameCodeActions
{item.name} + {item.country_code} + + + · + +
+ {/if} +
+
+
diff --git a/src/routes/(protected)/admin/general/+page.svelte b/src/routes/(protected)/admin/general/+page.svelte new file mode 100644 index 0000000..80f10ae --- /dev/null +++ b/src/routes/(protected)/admin/general/+page.svelte @@ -0,0 +1,16 @@ + + + + General — Admin — Trips + + +
+
+

General

+

General administration settings.

+
+ +
+ Administration settings will go here. +
+
diff --git a/src/routes/(protected)/admin/tour-operators/+page.svelte b/src/routes/(protected)/admin/tour-operators/+page.svelte new file mode 100644 index 0000000..ce27410 --- /dev/null +++ b/src/routes/(protected)/admin/tour-operators/+page.svelte @@ -0,0 +1,487 @@ + + + + Tour Operators — Admin — Trips + + +
+
+

Tour Operators

+

+ Manage known tour operators, their websites, and highlight colours for plan cards. +

+
+ +
+ +
+ + +
+ + + {#if adding || editing} +
+

+ {editing ? 'Edit operator' : 'Add operator'} +

+ {#if error} +

{error}

+ {/if} + +
+ +
+ +
+ onNameInput(e.currentTarget.value)} + onfocus={() => (showNameDropdown = true)} + onblur={() => setTimeout(() => (showNameDropdown = false), 150)} + placeholder="Search or type operator name" + class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none" + /> + {#if showNameDropdown && filteredOperators.length > 0} +
    + {#each filteredOperators as op} +
  • + +
  • + {/each} +
+ {/if} +
+
+ + +
+ + +
+ + +
+ +
+ + +
+
+ + +
+ Logo preview +
+ {#if newName && logoCache[newName] && logoCache[newName] !== 'none'} + {newName} + {:else} + None + {/if} +
+
+
+ +
+ + +
+
+ {/if} + + + {#if loading} +

Loading...

+ {:else if items.length === 0} +
+ + + + + + +

No tour operators yet. Add one to get started.

+
+ {:else} +
+ {#each items as item (item.id)} + {@const logoType = logoCache[item.name]} + {@const logoUrl = getLogoUrl(item.name)} + {@const borderColor = item.highlight_color ?? '#E5E7EB'} +
+ +
+
+ {#if logoType && logoType !== 'none' && logoUrl} + {item.name} + {:else} + + + + + + + {/if} +
+
+

{item.name}

+ {#if item.website} + + {item.website.replace(/^https?:\/\//, '')} + + {:else} +

No website

+ {/if} +
+
+ + +
+
+ {borderColor} + {#if logoType === 'square' || logoType === 'wide'} + + {logoType} logo + + {:else if logoType === 'none'} + + No logo + + {/if} +
+ + +
+ + +
+
+ {/each} +
+ {/if} +
+
diff --git a/src/routes/(protected)/api/tour-operators/+server.ts b/src/routes/(protected)/api/tour-operators/+server.ts new file mode 100644 index 0000000..3d896a4 --- /dev/null +++ b/src/routes/(protected)/api/tour-operators/+server.ts @@ -0,0 +1,8 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { listTourOperators } from '$lib/server/admin/data.js'; + +export const GET: RequestHandler = ({ url }) => { + const q = url.searchParams.get('q') ?? undefined; + return json(listTourOperators(q)); +}; diff --git a/src/routes/(protected)/trips/[id]/+page.server.ts b/src/routes/(protected)/trips/[id]/+page.server.ts index 2e1f5d4..8d0d93b 100644 --- a/src/routes/(protected)/trips/[id]/+page.server.ts +++ b/src/routes/(protected)/trips/[id]/+page.server.ts @@ -9,6 +9,11 @@ import { } from '$lib/server/travellers.js'; import { createFlight, updateFlight, getFlightBookingsForTrip } from '$lib/server/flights.js'; import { createLodging, updateLodging, getLodgingsForTrip } from '$lib/server/lodgings.js'; +import { + createPackageTour, + updatePackageTour, + getPackageToursForTrip +} from '$lib/server/package-tours.js'; import type { PageServerLoad, Actions } from './$types'; export const load: PageServerLoad = async (event) => { @@ -24,6 +29,7 @@ export const load: PageServerLoad = async (event) => { const people = getPeopleForUser(userId); const flightBookings = getFlightBookingsForTrip(trip.id, userId); const lodgings = getLodgingsForTrip(trip.id, userId); + const packageTours = getPackageToursForTrip(trip.id, userId); return { trip, @@ -33,7 +39,8 @@ export const load: PageServerLoad = async (event) => { travellerCount: travellers.length, people, flightBookings, - lodgings + lodgings, + packageTours }; }; @@ -184,6 +191,7 @@ export const actions: Actions = { if (!trip) return fail(404, { error: 'Trip not found' }); const data = await event.request.formData(); + const parentPlanId = (data.get('parent_plan_id') as string)?.trim() || undefined; const confirmationNumber = (data.get('confirmation_number') as string)?.trim() || undefined; const priceRaw = (data.get('price') as string)?.trim(); const price = priceRaw ? parseFloat(priceRaw) : undefined; @@ -282,6 +290,7 @@ export const actions: Actions = { createFlight({ tripId: trip.id, userId, + parentId: parentPlanId, confirmationNumber, price, currency, @@ -430,6 +439,7 @@ export const actions: Actions = { const v = str(key); return v ? parseFloat(v) : undefined; }; + const parentPlanId = str('parent_plan_id'); const status = data.get('status') as string as 'idea' | 'tentative' | 'confirmed'; const guestIds = data.getAll('guest_ids[]') as string[]; @@ -437,6 +447,7 @@ export const actions: Actions = { createLodging({ tripId: trip.id, userId, + parentId: parentPlanId, status: ['idea', 'tentative', 'confirmed'].includes(status) ? status : 'idea', name, chain: str('chain'), @@ -518,5 +529,94 @@ export const actions: Actions = { } catch (err) { return fail(400, { error: err instanceof Error ? err.message : 'Failed to update lodging' }); } + }, + + addPackageTour: async (event) => { + const session = await event.locals.auth(); + const userId = session?.user?.id; + if (!userId) return fail(401, { error: 'Not authenticated' }); + + const trip = getTripById(event.params.id, userId); + if (!trip) return fail(404, { error: 'Trip not found' }); + + const data = await event.request.formData(); + const operatorName = (data.get('operator_name') as string)?.trim(); + if (!operatorName) return fail(400, { error: 'Operator name is required' }); + + const str = (key: string) => (data.get(key) as string)?.trim() || undefined; + const num = (key: string) => { + const v = str(key); + return v ? parseFloat(v) : undefined; + }; + const status = data.get('status') as string as 'idea' | 'tentative' | 'confirmed'; + const travellerIds = data.getAll('traveller_ids[]') as string[]; + + try { + createPackageTour({ + tripId: trip.id, + userId, + status: ['idea', 'tentative', 'confirmed'].includes(status) ? status : 'idea', + operatorName, + confirmationNumber: str('confirmation_number'), + startDate: str('start_date'), + startTime: str('start_time'), + startTimezone: str('start_timezone'), + endDate: str('end_date'), + endTime: str('end_time'), + endTimezone: str('end_timezone'), + price: num('price'), + currency: str('currency') ?? 'USD', + travellerIds: travellerIds.length > 0 ? travellerIds : undefined + }); + return { success: true }; + } catch (err) { + return fail(400, { error: err instanceof Error ? err.message : 'Failed to create tour' }); + } + }, + + editPackageTour: async (event) => { + const session = await event.locals.auth(); + const userId = session?.user?.id; + if (!userId) return fail(401, { error: 'Not authenticated' }); + + const trip = getTripById(event.params.id, userId); + if (!trip) return fail(404, { error: 'Trip not found' }); + + const data = await event.request.formData(); + const tourId = (data.get('tour_id') as string)?.trim(); + if (!tourId) return fail(400, { error: 'Tour ID is required' }); + + const operatorName = (data.get('operator_name') as string)?.trim(); + if (!operatorName) return fail(400, { error: 'Operator name is required' }); + + const str = (key: string) => (data.get(key) as string)?.trim() || undefined; + const num = (key: string) => { + const v = str(key); + return v ? parseFloat(v) : undefined; + }; + const status = data.get('status') as string as 'idea' | 'tentative' | 'confirmed'; + const travellerIds = data.getAll('traveller_ids[]') as string[]; + + try { + updatePackageTour({ + tourId, + userId, + status: ['idea', 'tentative', 'confirmed'].includes(status) ? status : 'idea', + operatorName, + confirmationNumber: str('confirmation_number'), + startDate: str('start_date'), + startTime: str('start_time'), + startTimezone: str('start_timezone'), + endDate: str('end_date'), + endTime: str('end_time'), + endTimezone: str('end_timezone'), + price: num('price'), + currency: str('currency') ?? 'USD', + travellerIds: travellerIds.length > 0 ? travellerIds : undefined + }); + return { success: true }; + } catch (err) { + return fail(400, { error: err instanceof Error ? err.message : 'Failed to update tour' }); + } } }; diff --git a/src/routes/(protected)/trips/[id]/+page.svelte b/src/routes/(protected)/trips/[id]/+page.svelte index e700188..2962129 100644 --- a/src/routes/(protected)/trips/[id]/+page.svelte +++ b/src/routes/(protected)/trips/[id]/+page.svelte @@ -7,6 +7,9 @@ import EditFlightModal from '$lib/components/EditFlightModal.svelte'; import AddLodgingModal from '$lib/components/AddLodgingModal.svelte'; import EditLodgingModal from '$lib/components/EditLodgingModal.svelte'; + import AddPackageTourModal from '$lib/components/AddPackageTourModal.svelte'; + import EditPackageTourModal from '$lib/components/EditPackageTourModal.svelte'; + import PackageTourCard from '$lib/components/PackageTourCard.svelte'; import PlanCard from '$lib/components/PlanCard.svelte'; import FlightCard from '$lib/components/FlightCard.svelte'; import LodgingCard from '$lib/components/LodgingCard.svelte'; @@ -22,17 +25,21 @@ let tripTravellerIds = $derived(travellers.map((t) => t.id)); let flightBookings = $derived(data.flightBookings ?? []); let lodgings = $derived(data.lodgings ?? []); + let packageTours = $derived(data.packageTours ?? []); let editing = $state(false); let showAddDestination = $state(false); let showAddTraveller = $state(false); let showAddFlight = $state(false); let showAddLodging = $state(false); + let showAddPackageTour = $state(false); let showAddMenu = $state(false); let editingFlight = $state<(typeof flightBookings)[0] | null>(null); let editingFlightPlan = $derived( editingFlight ? (plans.find((p) => p.id === editingFlight!.plan_id) ?? null) : null ); let editingLodging = $state<(typeof lodgings)[0] | null>(null); + let editingTour = $state<(typeof packageTours)[0] | null>(null); + let addingChildToPlanId = $state(null); const menuItems = [ { @@ -77,6 +84,7 @@ label: 'Package Tours', icon: ``, onclick: () => { + showAddPackageTour = true; showAddMenu = false; } }, @@ -119,9 +127,13 @@ /> (showAddFlight = false)} + onclose={() => { + showAddFlight = false; + addingChildToPlanId = null; + }} {people} {tripTravellerIds} + parentPlanId={addingChildToPlanId ?? undefined} /> (showAddLodging = false)} + onclose={() => { + showAddLodging = false; + addingChildToPlanId = null; + }} {people} {tripTravellerIds} + parentPlanId={addingChildToPlanId ?? undefined} /> + (showAddPackageTour = false)} + {people} + {tripTravellerIds} +/> + (editingTour = null)} + {people} + {tripTravellerIds} +/>
@@ -546,6 +575,54 @@
{/if} + + + {#if packageTours.length > 0} +
+

+ Package Tours +

+
+ {#each packageTours as tour (tour.id)} + {@const plan = plans.find((p) => p.id === tour.plan_id)} + {#if plan} + {@const formId = `remove-plan-${plan.id}`} + {@const submitForm = () => { + const f = document.getElementById(formId) as HTMLFormElement; + f?.requestSubmit(); + }} +
{ + return ({ update }) => { + update(); + }; + }} + class="contents" + > + + (editingTour = tour)} + onDelete={submitForm} + onAddFlight={() => { + addingChildToPlanId = plan.id; + showAddFlight = true; + }} + onAddLodging={() => { + addingChildToPlanId = plan.id; + showAddLodging = true; + }} + /> + + {/if} + {/each} +
+
+ {/if} {/if} {/if}
diff --git a/src/tests/helpers.ts b/src/tests/helpers.ts new file mode 100644 index 0000000..14aae95 --- /dev/null +++ b/src/tests/helpers.ts @@ -0,0 +1,16 @@ +import { createSqliteDb } from '$lib/server/db/sqlite.js'; +import { runMigrations } from '$lib/server/db/migrations.js'; +import { _setDb } from '$lib/server/db/index.js'; +import type { Database } from '$lib/server/db/types.js'; + +/** + * Creates a fresh in-memory SQLite database, runs all migrations, and injects + * it into the server module singleton. Call in beforeEach; call db.close() in + * afterEach to release the in-memory DB. + */ +export function setupTestDb(): Database { + const database = createSqliteDb(':memory:'); + runMigrations(database); + _setDb(database); + return database; +} diff --git a/src/tests/stubs/env.ts b/src/tests/stubs/env.ts new file mode 100644 index 0000000..9b7b100 --- /dev/null +++ b/src/tests/stubs/env.ts @@ -0,0 +1,3 @@ +// Stub for $env/dynamic/private — not available outside SvelteKit at test time. +// Tests bypass createDb() entirely via _setDb(), so this is never actually read. +export const env: Record = {}; diff --git a/src/tests/stubs/paths.ts b/src/tests/stubs/paths.ts new file mode 100644 index 0000000..bdc2d6f --- /dev/null +++ b/src/tests/stubs/paths.ts @@ -0,0 +1,3 @@ +// Stub for $app/paths — used by some server helpers for URL construction. +export const base = ''; +export const assets = ''; diff --git a/static/package-tour-logos/g_adventures_square.svg b/static/package-tour-logos/g_adventures_square.svg new file mode 100644 index 0000000..9f2fcc9 --- /dev/null +++ b/static/package-tour-logos/g_adventures_square.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/package-tour-logos/g_adventures_wide.svg b/static/package-tour-logos/g_adventures_wide.svg new file mode 100644 index 0000000..707d938 --- /dev/null +++ b/static/package-tour-logos/g_adventures_wide.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..beb1e34 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vitest/config'; +import { resolve } from 'path'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/**/*.test.ts'], + coverage: { + provider: 'v8', + include: ['src/lib/server/**/*.ts'], + exclude: ['src/lib/server/db/**'] + } + }, + resolve: { + alias: { + $lib: resolve(__dirname, 'src/lib'), + '$env/dynamic/private': resolve(__dirname, 'src/tests/stubs/env.ts'), + '$app/paths': resolve(__dirname, 'src/tests/stubs/paths.ts') + } + } +});