From 7a75b863378b5eb4bcb65652ebbc4b37d738fc9e Mon Sep 17 00:00:00 2001 From: Hasan Mohammad Fawzi Date: Wed, 9 Sep 2026 15:09:33 +0200 Subject: [PATCH 01/21] Update README.md --- README.md | 218 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 217 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 31a91ab..0c9e865 100644 --- a/README.md +++ b/README.md @@ -21,4 +21,220 @@ Before a pull request can be merged: or explain why not, then click *Resolve conversation*. 3. **Pushing new commits dismisses the approval**, so if you change something after being approved, ask for another review. This is deliberate: what was - approved is what gets merged. \ No newline at end of file + approved is what gets merged. + + + # Turvær Norge + +Prosjekt 1 i **IT2810 Webutvikling** ved NTNU. + +Turvær Norge er en responsiv React-applikasjon som henter oppdaterte værdata for norske byer. Brukeren kan velge aktivitet, sammenligne byer og finne ut hvor været passer best til for eksempel gåtur, sykling eller utendørsaktiviteter. + +## Konsept + +Applikasjonen skal: + +- hente ferske værdata fra et åpent REST API +- vise værinformasjon for minst 8 norske byer +- la brukeren velge aktivitet +- filtrere og sortere byer etter værforhold +- vise temperatur, nedbør, vind og værtype +- vise tydelig lasting og feilmelding ved API-feil +- fungere på både mobil og desktop + +Værdata hentes direkte når applikasjonen brukes og mellomlagres ikke. + +## Dataflyt + +```mermaid +flowchart LR + A[Brukeren velger aktivitet] --> B[React oppdaterer state] + B --> C[Værtjenesten henter data] + C --> D[Vær-API] + D --> E[Filtrering og sortering] + E --> F[Værkort og detaljvisning] +``` + +## Komponentflyt + +```mermaid +flowchart TD + App --> ActivitySelector + App --> WeatherFilters + App --> WeatherList + WeatherList --> WeatherCard + App --> WeatherDetails + App --> LoadingMessage + App --> ErrorMessage +``` + +## Teknologi + +- React +- TypeScript +- Vite +- Node.js 24+ +- npm +- ESLint +- REST API med `fetch` +- plain CSS + +Alle UI-komponenter utvikles av gruppen. Prosjektet bruker ikke Bootstrap, Tailwind, Material UI eller andre ferdige UI-komponentbiblioteker. + +## Lokal oppstart etter kloning + +### Forutsetninger + +Installer Git og Node.js 24 eller nyere. npm følger med Node.js. + +```powershell +git --version +node --version +npm --version +``` + +### Klon og start prosjektet + +```powershell +git clone +cd +npm install +npm run dev +``` + +Vite viser adressen til applikasjonen, vanligvis `http://localhost:5173`. + +### Kvalitetskontroll + +Kjør alltid dette før du pusher: + +```powershell +npm run lint +npm run build +``` + +## Git-arbeidsflyt + +Vi bruker `main` og korte feature-brancher. Det skal ikke pushes direkte til `main`. + +```mermaid +flowchart LR + A[Oppdater main] --> B[Opprett feature-branch] + B --> C[Implementer ett issue] + C --> D[Kjør lint og build] + D --> E[Push branch] + E --> F[Merge request og review] + F --> G[Merge til main] +``` + +### 1. Start en ny oppgave + +Erstatt nummer og navn med det aktuelle issuet: + +```powershell +git switch main +git pull --ff-only origin main +git switch -c feature/issue-8-weather-card +``` + +### 2. Kontroller endringene + +```powershell +git status +git diff +npm run lint +npm run build +``` + +### 3. Commit og push + +Legg helst bare til filene som tilhører oppgaven: + +```powershell +git add src/components/WeatherCard.tsx src/styles/WeatherCard.css +git commit -m "feat: add weather card" +git push -u origin feature/issue-8-weather-card +``` + +### 4. Opprett merge request i GitLab + +1. Åpne **Merge requests**. +2. Opprett en merge request fra feature-branchen til `main`. +3. Legg inn `Closes #8` i beskrivelsen. +4. Velg en annen student som reviewer. +5. Rett eventuelle kommentarer og push på nytt. +6. Merge først når review, lint og build er godkjent. +7. Velg **Delete source branch** etter merge. + +### 5. Fortsett etter merge + +```powershell +git switch main +git pull --ff-only origin main +git branch -d feature/issue-8-weather-card +``` + +## Oppdatere en feature-branch + +Hvis andre har merget endringer til `main` mens du arbeider: + +```powershell +git fetch origin +git switch feature/issue-8-weather-card +git merge origin/main +``` + +Hvis Git melder om konflikt, ikke gjett. Åpne filene, avklar hvilket innhold som skal beholdes, og kjør deretter: + +```powershell +git add +git commit +git push +``` + +## Commit-meldinger + +```text +feat: add activity selector +fix: handle failed weather request +style: improve mobile weather cards +docs: update local setup instructions +chore: configure lint command +``` + +## Viktige sikkerhetsregler + +- Ikke push direkte til `main`. +- Ikke bruk `git push --force` på delte brancher. +- Ikke commit passord, tokens, API-nøkler eller `.env`-filer. +- Kjør `git status` og se gjennom `git diff` før commit. +- Commit bare filer som hører til issuet du arbeider med. +- Ikke bruk `git reset --hard` eller slett brancher når du er usikker. +- Ikke godkjenn din egen merge request; en annen student skal kontrollere den. +- Ikke merge kode som feiler på lint eller build. +- Avklar med gruppen før du endrer felles konfigurasjonsfiler. +- Be om hjelp før du løser en stor merge-konflikt. + +## Før en oppgave regnes som ferdig + +- [ ] Funksjonen virker lokalt. +- [ ] Akseptansekriteriene i issuet er oppfylt. +- [ ] TypeScript og ESLint gir ingen feil. +- [ ] `npm run build` er vellykket. +- [ ] Mobil- og desktopvisning er kontrollert når det er relevant. +- [ ] Koden er pushet på en feature-branch. +- [ ] Merge request er gjennomgått av en annen student. +- [ ] Dokumentasjonen er oppdatert ved behov. + +## Bygge prosjektet + +```powershell +npm run build +``` + +Den ferdige applikasjonen genereres i `dist`-mappen. Ved publisering på NTNU-serveren skal Vite konfigureres med riktig base-path for `/project1/`. + +## Bidrag + +Velg et ledig issue før du begynner. Ett issue bør normalt løses i én feature-branch og én merge request. Små og fokuserte merge requests er enklere å forstå, teste og gjennomgå. + From a335ec4dd227b4395bb7a726e471512a70f7d39d Mon Sep 17 00:00:00 2001 From: Hasan Mohammad Fawzi Date: Thu, 10 Sep 2026 11:11:00 +0200 Subject: [PATCH 02/21] Create roadmap.md --- roadmap.md | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 roadmap.md diff --git a/roadmap.md b/roadmap.md new file mode 100644 index 0000000..2fa7482 --- /dev/null +++ b/roadmap.md @@ -0,0 +1,125 @@ +# Roadmap – Prosjekt 1 + +Målet er å levere en fungerende og publisert prototype av **Turvær Norge** innen 18. september 2026. + +## Fase 1 – Planlegging og grunnoppsett + +**Mål:** Bli enige om minimumsløsningen og få React-prosjektet til å kjøre. + +- [ ] #1 Avklare minimumsløsningen +- [ ] #2 Sette opp React, TypeScript og Vite +- [ ] #4 Undersøke og velge vær-API + +**Ferdig når:** + +- Gruppen har valgt byer, aktiviteter og funksjoner. +- `npm run dev` fungerer. +- Vær-API-et er valgt og testet i nettleseren. + +--- + +## Fase 2 – Datagrunnlag og API + +**Mål:** Hente ekte værdata og gjøre dem tilgjengelige for React-komponentene. + +- [ ] #3 Definere TypeScript-modeller og bydata +- [ ] #5 Implementere `weatherService` +- [ ] #12 Lage loading-, error- og empty-state + +**Ferdig når:** + +- Appen henter ferske værdata. +- Dataene konverteres til egne TypeScript-typer. +- Lasting og API-feil håndteres uten at appen krasjer. + +--- + +## Fase 3 – Grunnleggende brukergrensesnitt + +**Mål:** Vise værdata på en forståelig måte. + +- [ ] #6 Lage grunnlayout og state i `App` +- [ ] #7 Lage `WeatherList` og `WeatherCard` +- [ ] #8 Lage aktivitetsvelger +- [ ] #11 Lage detaljvisning for valgt by + +**Ferdig når:** + +- Minst 8 byer vises som værkort. +- Brukeren kan velge aktivitet. +- Brukeren kan åpne detaljer for en by. +- Komponentene bruker typed props og React-state. + +--- + +## Fase 4 – Logikk og brukerfunksjoner + +**Mål:** Hjelpe brukeren med å finne byen som passer best. + +- [ ] #9 Implementere aktivitetsvurdering +- [ ] #10 Implementere filtrering og sortering + +**Ferdig når:** + +- Appen vurderer om været passer til valgt aktivitet. +- Byene kan filtreres og sorteres. +- `map`, `filter` og `sort` brukes uten å endre originaldata. + +--- + +## Fase 5 – Design og kvalitet + +**Mål:** Gjøre løsningen responsiv, tilgjengelig og klar for vurdering. + +- [ ] #13 Lage responsivt design med plain CSS +- [ ] #14 Kontrollere tilgjengelighet +- [ ] #15 Oppdatere README og dokumentere KI-bruk + +**Ferdig når:** + +- Appen fungerer på mobil og desktop. +- Hele appen kan brukes med tastatur. +- `npm run lint` og `npm run build` er vellykket. +- README og KI-bruk er dokumentert. + +--- + +## Fase 6 – Publisering og levering + +**Mål:** Publisere og kontrollere den endelige prototypen. + +- [ ] #16 Publisere prosjektet på NTNU-VM +- [ ] #17 Gjennomføre sluttkontroll og levering + +**Ferdig når:** + +- Appen fungerer under `/project1/`. +- API-kall og ressurser fungerer på NTNU-VM. +- Timelister og dokumentasjon er oppdatert. +- Alle krav er kontrollert mot Canvas. +- Gruppen er klar for medstudentvurdering. + +--- + +## Avhengigheter + +```mermaid +flowchart TD + I1[#1 Minimumsløsning] --> I3[#3 TypeScript-modeller] + I2[#2 React og Vite] --> I6[#6 App og state] + I4[#4 Velge API] --> I5[#5 weatherService] + I3 --> I5 + I5 --> I12[#12 Loading og error] + I6 --> I7[#7 WeatherList og WeatherCard] + I6 --> I8[#8 Aktivitetsvelger] + I7 --> I11[#11 Detaljvisning] + I5 --> I9[#9 Aktivitetsvurdering] + I8 --> I9 + I7 --> I10[#10 Filter og sortering] + I9 --> I10 + I7 --> I13[#13 Responsivt design] + I8 --> I13 + I13 --> I14[#14 Tilgjengelighet] + I14 --> I16[#16 Publisering] + I15[#15 Dokumentasjon] --> I17[#17 Sluttkontroll] + I16 --> I17 From e010fbd0fe30263eb9f527ab55e58456db5652da Mon Sep 17 00:00:00 2001 From: Oliver Dragland Date: Fri, 11 Sep 2026 11:26:51 +0200 Subject: [PATCH 03/21] init --- .dockerignore | 8 + .env.example | 7 + .gitattributes | 2 + .github/ISSUE_TEMPLATE/task.md | 21 + .github/pull_request_template.md | 15 + .gitignore | 13 + .node-version | 1 + .npmrc | 2 + .nvmrc | 1 + .prettierignore | 7 + .prettierrc.json | 1 + AGENTS.md | 9 + CHANGELOG.md | 9 + Dockerfile | 10 + README.md | 356 +- compose.yaml | 10 + docs/ai-use.md | 9 + docs/requirements.md | 40 + docs/testing.md | 25 + e2e/weather.spec.ts | 24 + eslint.config.js | 56 + package-lock.json | 9626 +++++++++++++++++ package.json | 79 + playwright.config.ts | 20 + project2/api/drizzle.config.ts | 7 + .../migrations/0000_flippant_barracuda.sql | 8 + .../api/migrations/meta/0000_snapshot.json | 59 + project2/api/migrations/meta/_journal.json | 13 + project2/api/package.json | 19 + project2/api/src/app.test.ts | 69 + project2/api/src/app.ts | 80 + project2/api/src/db/repository.ts | 39 + project2/api/src/db/schema.ts | 21 + project2/api/src/env.ts | 23 + project2/api/src/generated.ts | 226 + project2/api/src/schema.graphql | 15 + project2/api/src/server.ts | 31 + project2/api/tsconfig.build.json | 10 + project2/api/tsconfig.json | 10 + project2/contracts/generated.ts | 186 + project2/contracts/operations.graphql | 17 + roadmap.md | 1 + scripts/copy-schema.mjs | 5 + scripts/database-smoke.ts | 13 + scripts/database.ts | 49 + scripts/env-init.mjs | 11 + scripts/generate-api.ts | 50 + tsconfig.base.json | 15 + tsconfig.tools.json | 18 + vitest.config.ts | 31 + web/.gitignore | 24 + web/index.html | 13 + web/package.json | 29 + web/public/.gitkeep | 0 web/src/WeatherExample.test.tsx | 67 + web/src/WeatherExample.tsx | 63 + .../WeatherExample.test.tsx.snap | 78 + web/src/features/weather/cities.ts | 18 + web/src/features/weather/weather.ts | 58 + web/src/main.tsx | 15 + web/src/shared/storage.ts | 31 + web/src/styles.css | 64 + web/src/test/fixtures.ts | 17 + web/src/test/server.ts | 7 + web/src/test/setup.ts | 14 + web/tsconfig.app.json | 27 + web/tsconfig.json | 1 + web/vite.config.ts | 9 + 68 files changed, 11735 insertions(+), 177 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/task.md create mode 100644 .github/pull_request_template.md create mode 100644 .gitignore create mode 100644 .node-version create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md create mode 100644 Dockerfile create mode 100644 compose.yaml create mode 100644 docs/ai-use.md create mode 100644 docs/requirements.md create mode 100644 docs/testing.md create mode 100644 e2e/weather.spec.ts create mode 100644 eslint.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 playwright.config.ts create mode 100644 project2/api/drizzle.config.ts create mode 100644 project2/api/migrations/0000_flippant_barracuda.sql create mode 100644 project2/api/migrations/meta/0000_snapshot.json create mode 100644 project2/api/migrations/meta/_journal.json create mode 100644 project2/api/package.json create mode 100644 project2/api/src/app.test.ts create mode 100644 project2/api/src/app.ts create mode 100644 project2/api/src/db/repository.ts create mode 100644 project2/api/src/db/schema.ts create mode 100644 project2/api/src/env.ts create mode 100644 project2/api/src/generated.ts create mode 100644 project2/api/src/schema.graphql create mode 100644 project2/api/src/server.ts create mode 100644 project2/api/tsconfig.build.json create mode 100644 project2/api/tsconfig.json create mode 100644 project2/contracts/generated.ts create mode 100644 project2/contracts/operations.graphql create mode 100644 scripts/copy-schema.mjs create mode 100644 scripts/database-smoke.ts create mode 100644 scripts/database.ts create mode 100644 scripts/env-init.mjs create mode 100644 scripts/generate-api.ts create mode 100644 tsconfig.base.json create mode 100644 tsconfig.tools.json create mode 100644 vitest.config.ts create mode 100644 web/.gitignore create mode 100644 web/index.html create mode 100644 web/package.json create mode 100644 web/public/.gitkeep create mode 100644 web/src/WeatherExample.test.tsx create mode 100644 web/src/WeatherExample.tsx create mode 100644 web/src/__snapshots__/WeatherExample.test.tsx.snap create mode 100644 web/src/features/weather/cities.ts create mode 100644 web/src/features/weather/weather.ts create mode 100644 web/src/main.tsx create mode 100644 web/src/shared/storage.ts create mode 100644 web/src/styles.css create mode 100644 web/src/test/fixtures.ts create mode 100644 web/src/test/server.ts create mode 100644 web/src/test/setup.ts create mode 100644 web/tsconfig.app.json create mode 100644 web/tsconfig.json create mode 100644 web/vite.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b956746 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +**/node_modules +**/dist +.env +.env.* +coverage +playwright-report +test-results diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6eeace6 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Used only by the optional Project 2 API and database commands. +# Create this dedicated native PostgreSQL database and role using README.md. +DATABASE_URL=postgresql://t31_dev:change-me@127.0.0.1:5432/t31_project2_dev +API_HOST=127.0.0.1 +API_PORT=3001 +CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 +NODE_ENV=development diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..90b7e53 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# DOM snapshots intentionally preserve rendered text-node spaces. +*.snap whitespace=-blank-at-eol diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md new file mode 100644 index 0000000..922cc70 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.md @@ -0,0 +1,21 @@ +--- +name: Development task +about: Plan a small, reviewable course task +title: '' +labels: '' +assignees: '' +--- + +## Outcome + +What should the user be able to do? + +## Course requirement + +Link the requirement and explain the chosen approach. + +## Acceptance checks + +- [ ] Functionality and error states verified +- [ ] Relevant tests and documentation updated +- [ ] Teammate review completed diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..b3b0bb7 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +## Change + +Explain the user-visible problem and resulting behavior. Link the issue: Closes #... + +## Verification + +List commands actually run and relevant manual/browser checks. + +## Course requirements and AI use + +Describe affected requirements, architectural decisions, AI assistance, and how the team verified it. + +## Review + +Request a teammate review. Record follow-up work; do not self-approve. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..638e9db --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules/ +dist/ +coverage/ +playwright-report/ +test-results/ +.env +.env.* +!.env.example +*.local +*.tsbuildinfo +bun.lock +bun.lockb +.DS_Store diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..df6ae33 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24.21.0 diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..d9ca886 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +engine-strict=true +save-exact=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..df6ae33 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.21.0 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..6e4d347 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +node_modules +dist +coverage +playwright-report +test-results +package-lock.json +project2/api/migrations/meta diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..6452287 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{ "singleQuote": true, "trailingComma": "all" } diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0375730 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ +# T31 development guide + +Read README.md and docs/requirements.md before changing architecture. Project 1 is a static React/Vite app with plain CSS, self-authored components, TanStack Query, live REST data, and Web Storage for preferences/favorite IDs only. Do not connect it to the Project 2 database or cache API resources. + +Project 2 foundation lives separately in project2/. Its Node/TypeScript GraphQL API and native PostgreSQL database must run directly on the group VM. No Docker backend/database deployment or hosted Supabase. Move this foundation to the assigned Project 2 repository when that work starts. + +Use Node 24 and npm 11; package-lock.json is the installation source of truth. Bun may run scripts. Keep strict TypeScript, validate external data with Zod, infer database types from Drizzle, and regenerate GraphQL types with npm run types:api. Never hand-edit generated.ts or generated migration snapshots. + +Use issues, feature branches and teammate-reviewed PRs. Preserve existing changes. Never commit .env or personal contribution logs. Document AI assistance and actual checks. Run npm run check for code changes and relevant Playwright checks for UI changes. Never claim grade A, device testing, deployment, review, or remote publication without evidence. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e9e1c8e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## 2026-09-11 + +- Initial Turvær Norge architecture skeleton with one live weather example. +- Separate native PostgreSQL/Node GraphQL foundation for Project 2. +- Typed contracts, developer scripts, tests, requirement traceability and setup docs. + +Record post-delivery improvements and responses to feedback here. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ce299b6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM node:24.21.0-bookworm-slim +WORKDIR /app +RUN npm install --global npm@11.11.1 +COPY package.json package-lock.json ./ +COPY web/package.json web/package.json +COPY project2/api/package.json project2/api/package.json +RUN npm ci +COPY . . +EXPOSE 5173 +CMD ["npm", "run", "dev", "--workspace", "@t31/web", "--", "--host", "0.0.0.0"] diff --git a/README.md b/README.md index 0c9e865..95b9ae1 100644 --- a/README.md +++ b/README.md @@ -1,240 +1,242 @@ -# T31-Project-1 +# Turvær Norge — T31 architecture skeleton -Repository for team IT2810-H26-T31 in IT2810-H26. +## Development setup -## Working in this repository +Use Node **24.6+ within the 24 series** and npm **11**. The checkout pins Node 24.21.0; Node 22 does not satisfy the assignment. -`main` is protected — you cannot push to it directly. Work on a branch and -open a pull request: +```bash +git clone --branch dev git@git.ntnu.no:IT2810-H26/T31-Project-1.git +cd T31-Project-1 +nvm install +nvm use +npm install --global npm@11.11.1 +npm ci +npm run dev +``` + +Open . If nvm is not installed, follow the [Node installation guide](https://nodejs.org/en/download) or use fnm with `.node-version`. + +Bun is an optional script launcher: `bun dev`, `bun types:api`, `bun db:reset`, and `bun run check` work with Node/npm available on PATH. The course explicitly specifies Node and npm; retain `package-lock.json` and use **npm ci** to install. Do not maintain competing Bun/npm lockfiles. `bun test` invokes Bun's own runner, so use **bun run test** for required Vitest tests. If Bun is installed at `~/.bun/bin/bun`, add `export PATH="$HOME/.bun/bin:$PATH"` to your shell configuration if needed. + +### Everyday development tools ```bash -git checkout -b my-feature -git push -u origin my-feature +npm run dev # Single weather example: http://127.0.0.1:5173/project1/ +npm run test:watch # Vitest while editing +npm run check # Formatting, lint, types, tests and builds +npm run format # Apply Prettier +npm run types:api # Regenerate the optional GraphQL contracts +npm run build # Static client output: web/dist/ +npm run preview # Preview that production build ``` -Before a pull request can be merged: +Optional Project 2 work, after the native PostgreSQL setup below: -1. **A member of IT2810-H26-T31 must approve it.** You cannot approve your own pull - request, and approvals from staff do not count — you review each other's - code. -2. **Every review comment must be marked resolved.** Reply to each one, fix it - or explain why not, then click *Resolve conversation*. -3. **Pushing new commits dismisses the approval**, so if you change something - after being approved, ask for another review. This is deliberate: what was - approved is what gets merged. +```bash +npm run env:init +npm run db:migrate +npm run db:seed +npm run dev:api # Node GraphQL API: http://127.0.0.1:3001/graphql +npm run dev:all # Example frontend and API together +npm run db:reset # DESTRUCTIVE: dedicated local development schemas only +``` +Bun equivalents include `bun dev`, `bun types:api`, `bun db:reset` and `bun run check`. Keep Node 24/npm 11 available even when launching commands with Bun. The full command reference and Docker alternative are below. - # Turvær Norge +## Scope -Prosjekt 1 i **IT2810 Webutvikling** ved NTNU. +Architecture skeleton for **Turvær Norge**, the team's weather/activity project described on the remote `dev` branch. The eventual app will help users choose an outdoor activity and compare weather across at least eight Norwegian cities. See the preserved [team roadmap](roadmap.md). -Turvær Norge er en responsiv React-applikasjon som henter oppdaterte værdata for norske byer. Brukeren kan velge aktivitet, sammenligne byer og finne ut hvor været passer best til for eksempel gåtur, sykling eller utendørsaktiviteter. +**Scope:** tooling, typed boundaries and **one example screen**, [`WeatherExample.tsx`](web/src/WeatherExample.tsx), fetching current weather for Trondheim. This is not the completed Project 1 app. City browsing, activity recommendations, filters, sorting, favorites, full component composition and final design belong to future issues. Eight typed city definitions are provided as a starting point for team agreement. -## Konsept +The separate `project2/` directory prepares native PostgreSQL and a Node/Hono/GraphQL API as requested. Project 1 runs without that backend, database, Docker, API keys or environment files. See [requirement traceability](docs/requirements.md), [testing](docs/testing.md) and [AI use](docs/ai-use.md). -Applikasjonen skal: +## Why this stack differs from the proposal -- hente ferske værdata fra et åpent REST API -- vise værinformasjon for minst 8 norske byer -- la brukeren velge aktivitet -- filtrere og sortere byer etter værforhold -- vise temperatur, nedbør, vind og værtype -- vise tydelig lasting og feilmelding ved API-feil -- fungere på både mobil og desktop +| Technology | Decision | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| React, TypeScript, Vite | Required. Scaffolded with `npm create vite@latest -- --template react-ts`; React Compiler added per the environment notes. | +| TanStack Query | Required for Project 1 REST requests. | +| Plain CSS, original components | Required for Project 1. No Tailwind, shadcn, or component library. | +| Hono | Optional Project 2 HTTP host; GraphQL Yoga supplies the required GraphQL API. Uses Node in development and deployment. | +| PostgreSQL + Drizzle | Project 2 requires a database installed directly on the group VM. PostgreSQL meets this; Drizzle gives typed queries and reviewable SQL migrations. | +| Supabase | Not required. Hosted Supabase conflicts with the VM database requirement; its typical Docker deployment conflicts with the direct-install rule. Use native PostgreSQL. | +| Redis | Omitted. Project 1 forbids storing API resources. Project 2 does not require Redis; introduce caching only for a measured need later. | +| Docker | Optional frontend development convenience only. Both delivered clients must be served by Apache; Project 2 backend/database must run directly on the VM. | +| Bun | Optional script runner; Node 24/npm 11 remain the documented reproducible baseline. | -Værdata hentes direkte når applikasjonen brukes og mellomlagres ikke. +The bundled Project 2 specification says **under work**. Recheck Canvas before expanding it. Do not assume later assignments permit Project 1's choices or vice versa. -## Dataflyt +## Architecture and type safety -```mermaid -flowchart LR - A[Brukeren velger aktivitet] --> B[React oppdaterer state] - B --> C[Værtjenesten henter data] - C --> D[Vær-API] - D --> E[Filtrering og sortering] - E --> F[Værkort og detaljvisning] +```text +web/src/ + features/weather/ Typed city definitions, validated REST boundary, Query options + shared/storage.ts Validated, versioned browser preferences + test/ Offline fixtures and MSW request mocking + WeatherExample.tsx The single example screen: live Trondheim weather +project2/ + api/src/schema.graphql Single GraphQL contract source + api/src/generated.ts Generated resolver types + api/src/db/schema.ts Drizzle schema; inferred select/insert types + api/src/db/repository.ts Typed persistence boundary + api/migrations/ Generated, committed SQL and migration metadata + contracts/ Operations and generated typed documents for a future client +scripts/ Contract generation, safe local DB commands, environment setup ``` -## Komponentflyt - -```mermaid -flowchart TD - App --> ActivitySelector - App --> WeatherFilters - App --> WeatherList - WeatherList --> WeatherCard - App --> WeatherDetails - App --> LoadingMessage - App --> ErrorMessage -``` +The Bedrockchain reference informed strict TypeScript, runtime boundary validation, generated contracts with drift checking, inferred database types, root commands, and migration/reset/seed workflows. Its infrastructure and UI libraries were not copied because course requirements differ. -## Teknologi +- External REST JSON is `unknown` until Zod validates it. UI types are inferred from that schema. +- GraphQL SDL generates resolver types and typed client documents; `types:check` detects stale output without rewriting it. +- Database row/insert types derive from Drizzle (`NoteRow`/`NewNote`); `types:db` checks these rather than generating a duplicate Supabase type file. +- Strict mode, unchecked-index checking, exact optional properties, lint and formatting apply throughout. +- The example imports nothing from `project2/`. Its build contains no database credentials or backend code. -- React -- TypeScript -- Vite -- Node.js 24+ -- npm -- ESLint -- REST API med `fetch` -- plain CSS +## Project 1 data and state boundaries -Alle UI-komponenter utvikles av gruppen. Prosjektet bruker ikke Bootstrap, Tailwind, Material UI eller andre ferdige UI-komponentbiblioteker. +The example uses the [Open-Meteo forecast API](https://open-meteo.com/en/docs), a keyless REST provider proposed for issue #4. The typed city list proposes Oslo, Bergen, Trondheim, Stavanger, Tromsø, Bodø, Kristiansand and Ålesund. Confirm final cities/activities/API with the team; the skeleton does not close the planning issues. -## Lokal oppstart etter kloning +`features/weather/weather.ts` requests current temperature, precipitation, wind speed and WMO weather code. Wind units are explicitly **m/s**, timestamps use **Europe/Oslo**, and precipitation is labelled with the provider's interval (currently 15 minutes), not presented as a daily total. The Zod schema rejects malformed values or unexpected units before data reaches the UI. The example displays three metrics; mapping WMO codes to human-readable weather types is future domain work. -### Forutsetninger +TanStack Query holds the active response in memory, with no persistent query cache. `gcTime: 0` discards unobserved results, `cache: no-store` avoids browser HTTP storage, reload fetches fresh data, and focus/reconnect refetch and automatic retries are disabled. User retry is explicit. Development StrictMode can cancel/restart an initial request; production smoke tests check one request. Decide an explicit refresh policy when implementing long-lived weather views. -Installer Git og Node.js 24 eller nyere. npm følger med Node.js. +`shared/storage.ts` is a reusable validated storage hook for future preference and favorite-ID state. The example **does not implement favorites or persist weather**. Use namespaced/versioned keys, session storage for preferences and local storage for favorite city IDs; never store the weather payload. When building the full client, show one city resource at a time with previous/next and direct selection, as required by Project 1. The team's roadmap mentions multiple cards; adapt that presentation to the course requirement while keeping all eight cities selectable. -```powershell -git --version -node --version -npm --version -``` +CORS must be permitted by the external API; frontend headers cannot grant it. There is no local proxy hiding a production restriction. Project 2 has its own explicit origin allowlist and credentials disabled; CORS is not authentication. Open-Meteo attribution and the CC BY 4.0 link are present in the example. Review the provider's terms, noncommercial usage allowance and limits before delivery. -### Klon og start prosjektet +## Commands -```powershell -git clone -cd -npm install -npm run dev -``` +All commands below are `npm run ` or `bun run `; non-reserved names also support `bun `. + +| Command | Purpose | +| ----------------------------------------- | -------------------------------------------------------------------------------------- | +| `dev`, `dev:web` | Project 1 Vite server | +| `dev:api` | Optional Project 2 Node API with watch | +| `dev:all` | Both servers; requires local PostgreSQL configuration for API operations | +| `build`, `preview` | Build/preview Project 1 at `/project1/` | +| `build:api`, `start:api` | Compile/run the Project 2 Node API | +| `setup` | npm ci, preserve/create .env, generate API types | +| `env:init` | Copy .env.example only if .env is missing | +| `types:api`, `types:check` | Generate/check GraphQL resolver and operation types | +| `types:db`, `typecheck` | Inferred DB types / all strict TypeScript checks | +| `db:generate` | Generate a migration after changing Drizzle schema | +| `db:migrate` | Apply committed migrations to DATABASE_URL | +| `db:seed` | Idempotent development note seed | +| `db:reset` | Drop only project2/project2_migrations schemas, migrate, seed a guarded local database | +| `db:check`, `test:db` | Check migration metadata / verify a seeded native PostgreSQL read | +| `lint`, `lint:fix` | ESLint and optional fixes | +| `format`, `format:check` | Prettier write/check | +| `test`, `test:watch`, `test:coverage` | Vitest, always offline with mocks | +| `test:e2e:install`, `test:e2e` | Install browser engines / run Playwright and axe checks | +| `check` | Formatting, lint, contract drift, types, migration metadata, unit tests, both builds | +| `docker:up`, `docker:down`, `docker:logs` | Optional frontend-only development container | -Vite viser adressen til applikasjonen, vanligvis `http://localhost:5173`. +`check` does not require PostgreSQL or downloaded browsers. Run `test:db` and `test:e2e` separately when their prerequisites are available. -### Kvalitetskontroll +## Native PostgreSQL setup for Project 2 -Kjør alltid dette før du pusher: +Do this only when working on the optional backend. On Debian/Ubuntu install PostgreSQL directly: -```powershell -npm run lint -npm run build +```bash +sudo apt install postgresql postgresql-client +sudo systemctl start postgresql +sudo -u postgres createuser --pwprompt t31_dev +sudo -u postgres createdb --owner=t31_dev t31_project2_dev +npm run env:init ``` -## Git-arbeidsflyt +Edit `.env` with your chosen password (URL-encode special characters). Do not commit it. Then: -Vi bruker `main` og korte feature-brancher. Det skal ikke pushes direkte til `main`. +```bash +npm run db:migrate +npm run db:seed +npm run test:db +npm run dev:api +``` -```mermaid -flowchart LR - A[Oppdater main] --> B[Opprett feature-branch] - B --> C[Implementer ett issue] - C --> D[Kjør lint og build] - D --> E[Push branch] - E --> F[Merge request og review] - F --> G[Merge til main] +Open for development GraphiQL. `/health` reports the process; `/ready` checks the database. Try: + +```graphql +query { + notes(first: 10) { + id + body + createdAt + } +} +mutation { + createNote(body: "Our first persisted note") { + id + body + } +} ``` -### 1. Start en ny oppgave +This notes example demonstrates typed persistence and validation; it is not the finished Project 2 catalogue. It has no user authentication or authorization and binds to loopback by default. Before making it publicly writable, design identity/ownership, abuse controls, GraphQL query-cost limits, pagination and domain-specific behavior. -Erstatt nummer og navn med det aktuelle issuet: +`db:reset` is destructive for its two project schemas. It refuses production mode, non-loopback hosts, URL parameters and database names other than `t31_project2_dev`/`t31_project2_test`. Do not point a loopback tunnel at a remote database. It does not delete other schemas or reset system PostgreSQL. `db:migrate` is intentionally usable on the eventual VM deployment; inspect its target first. -```powershell -git switch main -git pull --ff-only origin main -git switch -c feature/issue-8-weather-card -``` +For schema changes: edit `schema.ts`, run `db:generate`, inspect SQL, migrate/reset a dedicated local DB, run `types:api` when SDL changes, and run `check` plus `test:db`. Commit migrations and generated contracts. `db:check` validates migration metadata, not live database drift. -### 2. Kontroller endringene +## Authentication and publishing to NTNU -```powershell -git status -git diff -npm run lint -npm run build -``` +Remote: `git@git.ntnu.no:IT2810-H26/T31-Project-1.git`. -### 3. Commit og push +NTNU GitHub Enterprise and github.com are separate hosts/accounts. Your github.com authentication does not automatically log you into NTNU, but the same SSH public key can be registered with both. -Legg helst bare til filene som tilhører oppgaven: +1. Sign in to with your school account and verify access to the T31 repo. +2. On this machine, run `cat ~/.ssh/id_ed25519.pub`. Register that **public** key at . Never share `id_ed25519` without `.pub`. +3. Run `ssh -T git@git.ntnu.no`. On first connection, verify the server fingerprint with NTNU before accepting. Successful GitHub SSH authentication may exit with status 1 because it does not offer shell access. +4. Run `git ls-remote origin` from this repository to verify repository access. -```powershell -git add src/components/WeatherCard.tsx src/styles/WeatherCard.css -git commit -m "feat: add weather card" -git push -u origin feature/issue-8-weather-card -``` +To keep distinct keys, use `ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_ntnu -C "your-ntnu-email"`, register its `.pub` file, then add this host-specific entry to `~/.ssh/config`: -### 4. Opprett merge request i GitLab +```sshconfig +Host git.ntnu.no + HostName git.ntnu.no + User git + IdentityFile ~/.ssh/id_ed25519_ntnu + IdentitiesOnly yes +``` -1. Åpne **Merge requests**. -2. Opprett en merge request fra feature-branchen til `main`. -3. Legg inn `Closes #8` i beskrivelsen. -4. Velg en annen student som reviewer. -5. Rett eventuelle kommentarer og push på nytt. -6. Merge først når review, lint og build er godkjent. -7. Velg **Delete source branch** etter merge. +This does not change github.com settings. If the key is passphrase-protected, add it to your agent with `ssh-add ~/.ssh/id_ed25519_ntnu`. -### 5. Fortsett etter merge +For issues/PRs through the CLI, authenticate separately with `gh auth login --hostname git.ntnu.no --git-protocol ssh --web`. An SSH key is enough for git push; it is not API/CLI authentication. If web/device login is unavailable on this Enterprise version, use its documented token flow in your own terminal. Do not paste tokens into chat. -```powershell -git switch main -git pull --ff-only origin main -git branch -d feature/issue-8-weather-card -``` +SSH was verified on this machine as `oliverdr`, using the dedicated `~/.ssh/id_ed25519_ntnu` key. Host-specific configuration preserves github.com access. No NTNU GitHub CLI/API login was assumed. -## Oppdatere en feature-branch +This initial `init` commit is prepared directly on `dev` at the user's request, preserving its existing concept, roadmap and CODEOWNERS history. Main remains protected. For subsequent work, create a feature branch and open a pull request following the team's integration process. This is **GitHub Enterprise**, so use Pull requests, not the GitLab Merge requests wording in the earlier README. -Hvis andre har merget endringer til `main` mens du arbeider: +The repository's review rules remain: -```powershell -git fetch origin -git switch feature/issue-8-weather-card -git merge origin/main -``` +1. A member of `IT2810-H26-T31` must approve; self-approval and staff approval do not count. +2. All review conversations must be resolved. +3. New commits dismiss existing approval; request another review after changes. +4. Never force-push shared branches or commit secrets. Select an issue before new feature work and link it in commits/PRs. -Hvis Git melder om konflikt, ikke gjett. Åpne filene, avklar hvilket innhold som skal beholdes, og kjør deretter: +Optionally configure your school commit email for this repo only: `git config user.email "your-ntnu-email"`. This affects commit attribution, not SSH authentication. -```powershell -git add -git commit -git push -``` +## Apache deployment -## Commit-meldinger +Project 1 must be served by Apache on the group VM at `/project1/`. `base: '/project1/'` is already configured. Campus network or NTNU VPN is required to reach the VM. Verify that the assigned hostname is `it2810-31.idi.ntnu.no` before using it. -```text -feat: add activity selector -fix: handle failed weather request -style: improve mobile weather cards -docs: update local setup instructions -chore: configure lint command +```bash +npm ci +npm run check +npm run test:e2e +# Set these to the confirmed account/host: +# scp -r web/dist YOUR_NTNU_USER@it2810-31.idi.ntnu.no:~/t31-project1-dist ``` -## Viktige sikkerhetsregler - -- Ikke push direkte til `main`. -- Ikke bruk `git push --force` på delte brancher. -- Ikke commit passord, tokens, API-nøkler eller `.env`-filer. -- Kjør `git status` og se gjennom `git diff` før commit. -- Commit bare filer som hører til issuet du arbeider med. -- Ikke bruk `git reset --hard` eller slett brancher når du er usikker. -- Ikke godkjenn din egen merge request; en annen student skal kontrollere den. -- Ikke merge kode som feiler på lint eller build. -- Avklar med gruppen før du endrer felles konfigurasjonsfiler. -- Be om hjelp før du løser en stor merge-konflikt. - -## Før en oppgave regnes som ferdig - -- [ ] Funksjonen virker lokalt. -- [ ] Akseptansekriteriene i issuet er oppfylt. -- [ ] TypeScript og ESLint gir ingen feil. -- [ ] `npm run build` er vellykket. -- [ ] Mobil- og desktopvisning er kontrollert når det er relevant. -- [ ] Koden er pushet på en feature-branch. -- [ ] Merge request er gjennomgått av en annen student. -- [ ] Dokumentasjonen er oppdatert ved behov. - -## Bygge prosjektet - -```powershell -npm run build -``` +On the VM, install Apache with `sudo apt install apache2`, then copy the **contents** of the built directory into `/var/www/html/project1/` (for example `sudo install -d /var/www/html/project1` followed by `sudo cp -a ~/t31-project1-dist/. /var/www/html/project1/`). Avoid nesting `dist` inside `project1`. Use a fresh staging directory per release; back up any existing deployment before replacing it. Verify `/project1/`, a direct reload, API access, weather loading, keyboard access and console output through Apache. This starter uses no client router, so no SPA rewrite rule is needed. + +Project 2's eventual client belongs at `/project2/`. Run its built API on Node, ideally as a systemd service with an environment file, and PostgreSQL directly on the VM. The preferred backend port is 3001. Apache can reverse-proxy `/project2-api/` to `http://127.0.0.1:3001/`; keep the API bound to loopback for that setup. A client then uses `/project2-api/graphql`. Service configuration, final domain, deployment permissions and credentials remain team tasks. -Den ferdige applikasjonen genereres i `dist`-mappen. Ved publisering på NTNU-serveren skal Vite konfigureres med riktig base-path for `/project1/`. +## Team process and delivery -## Bidrag +Plan changes in issues; link branches/commits/PRs; require teammate review and record verification. Each member should understand and improve generated code. Keep individual time/contribution logs out of Git and deliver them through the course's specified channel. -Velg et ledig issue før du begynner. Ett issue bør normalt løses i én feature-branch og én merge request. Små og fokuserte merge requests er enklere å forstå, teste og gjennomgå. +The downloaded Canvas schedule lists Project 1 delivery **18 September**, peer review **25 September**, and final delivery **27 November 2026**. Verify the live schedule. The materials disagree on some overall assessment percentages; use current Canvas/staff guidance. Meeting the requirement list alone is described as a satisfactory baseline, not an A guarantee. Excellent usability, design, technical understanding, testing and collaboration must be demonstrated over the project. +NTNU Enterprise does not supply hosted Actions runners. Do not assume `ubuntu-latest` will run. `npm run check` is ready for a future self-hosted runner; the course says runner setup may require staff-granted repository admin access. No nonfunctional workflow is installed here. diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..f45b1b4 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,10 @@ +name: t31-project1 +services: + web: + build: . + ports: + - '127.0.0.1:5173:5173' + volumes: + - ./web/src:/app/web/src:ro + - ./web/public:/app/web/public:ro + init: true diff --git a/docs/ai-use.md b/docs/ai-use.md new file mode 100644 index 0000000..cbe5044 --- /dev/null +++ b/docs/ai-use.md @@ -0,0 +1,9 @@ +# AI assistance log + +## 2026-09-11 — architecture skeleton + +The user asked Codex to read local IT2810 materials and a sibling Bedrockchain reference, adapt the proposed stack, and prepare typed architecture/developer scripts. They requested Project 2 database groundwork, then clarified that the deliverable must stay a skeleton with a single example file. + +After SSH access was established, Codex read the remote dev README and roadmap. The skeleton now follows the team's Turvær Norge concept with one Trondheim weather example and typed definitions for eight proposed cities. The final scope leaves Project 1 feature implementation to team issues. Native PostgreSQL and Node GraphQL replace the originally suggested Supabase/Docker backend to match course constraints. + +AI assisted with analysis, configuration, implementation, docs and automated checks. The team must review/understand generated code, confirm API/cities/activities, complete the roadmap and document each member's actual work. Automated checks are not a substitute for human review, real-device testing or demonstrated course knowledge. diff --git a/docs/requirements.md b/docs/requirements.md new file mode 100644 index 0000000..9ac8a52 --- /dev/null +++ b/docs/requirements.md @@ -0,0 +1,40 @@ +# Course requirements and skeleton scope + +Sources inspected on 11 September 2026: sibling `../prosjektbeskrivelser/prosjekt1.md`, `prosjekt2.md`, the downloaded Canvas `viewer/course-data.js` (project descriptions, environment, VM, Git, assessment and delivery pages), the three lecture PDFs, and the team's `origin/dev` README and roadmap. Project 2 is marked “under work”; project3.md is empty. Course downloads remain outside this repository. + +## Project 1 — Turvær Norge + +| Requirement | Skeleton support / work remaining | +| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| React, TypeScript, Vite | Latest create-vite React TS scaffold, strict configuration, React Compiler | +| Live REST and TanStack Query | One Trondheim weather example; Zod validation, cancellation, timeout, explicit retry; expand to eight cities later | +| At least eight cities (team concept) | Eight typed city definitions; one city rendered in the example | +| One resource, previous/next, direct selection | Not implemented; retain this course requirement when building the roadmap's WeatherList | +| Activity selection and weather assessment (team concept) | Not implemented; agree thresholds/activities in issues #1, #8 and #9 | +| Filtering/sorting persisted across reload | Storage utility provided; feature not implemented | +| Favorites across browser restarts | Storage utility provided; feature not implemented; store IDs only | +| Both Web Storage APIs | Hook supports local/session storage; integrate into real features later | +| Original components and plain CSS | Example uses native HTML/React and plain CSS; no UI library | +| Accessibility and responsive design | Semantic example, native retry button, status/error messages, fluid CSS; full app and device testing remain | +| Node 24.6+, npm 11+ | Pinned versions, strict engines, npm lockfile; Bun script convenience | +| ESLint and Prettier | Configured with strict types and zero-warning lint checks | +| Vitest, snapshot, mocked tests | Example data/error/retry/snapshot tests plus API tests; final prop/state/component coverage must grow with features | +| Mainstream browsers/mobile | Playwright Chromium/Firefox/WebKit and portrait/landscape projects; record actual runs, verify real devices later | +| Issues, branches, PRs, peer review | Initial commit on dev as requested; original CODEOWNERS/roadmap preserved; subsequent changes need team review | +| README and AI documentation | Included; update with actual team choices and evidence | +| Apache at /project1/ | Base path configured; actual VM deployment remains | +| Contribution notes, peer assessment, feedback fixes | Team/individual tasks; personal time logs stay out of Git | + +This is deliberately a skeleton with one weather example, not a completed requirement checklist. No weather payload is persisted or proxied through the optional backend. + +## Project 2 + +Database storage is required; Supabase is not. The database and Node/TypeScript GraphQL backend must run directly on the group VM, not in Docker. This foundation provides native PostgreSQL, Drizzle schema/migrations/inferred types, Hono + GraphQL Yoga, generated resolver and operation types, validated environment, CORS and lifecycle setup. A minimal notes table/API provides a persistence boundary for development; there is no Project 2 UI. + +Future work: move the reusable foundation to the assigned Project 2 repo, choose the domain/dataset, build the React client and state management, implement submitted/debounced case-insensitive search, paginate large result sets, filter/sort the entire server dataset before pagination, add detail views and meaningful user-generated data, design ownership/security and query limits, test scalability and sustainability, deploy native services and the /project2/ client, and document collaboration. + +The generic notes API does not establish catalogue scalability or authentication; do not publish it as a finished backend. + +## Assessment + +Canvas says meeting listed requirements is the satisfactory baseline. An A requires demonstrated understanding, excellent usability/design, thorough verification/polish and collaboration/individual knowledge. A skeleton cannot guarantee a grade. The downloaded materials disagree on some assessment percentages; confirm the current rubric/schedule with Canvas or staff. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..b0c4ab0 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,25 @@ +# Testing record + +## Skeleton verification — 2026-09-11 + +Results below are updated from actual command output. The example tests cover valid weather, snapshot output, provider failure/retry, invalid units/data, coordinates and cancellation. MSW fails unhandled network requests. API tests cover GraphQL execution, input validation, persistence boundaries, CORS and readiness failures. + +Playwright smoke tests intercept provider data and check the production example, one request, horizontal overflow and axe accessibility. This does not establish full-app usability or real-device coverage. The full Project 1 behavior has intentionally not been implemented. + +## Executed results + +- `npm run check`: passed formatting, zero-warning lint, GraphQL contract drift, strict TypeScript, migration metadata, 7 Vitest tests (including snapshot), frontend build and Node API build. +- `npm run test:e2e`: 5 passed in Chromium, Firefox, WebKit and emulated iPhone portrait/landscape. Axe found no violations in the example. Browser emulation is not physical-device testing. +- `npm run db:reset` and `npm run test:db`: passed against a fresh isolated native PostgreSQL 18 instance on loopback port 55439 with database `t31_project2_test`. The system PostgreSQL cluster and other projects were untouched; the temporary instance was stopped afterwards. +- Live Open-Meteo request: eight coordinate responses returned; a request with an Origin header received `Access-Control-Allow-Origin: *`. Default automated tests still use mocks. +- `docker compose config --quiet`: passed. The optional frontend image/container was not built or started during this verification. +- Verification toolchain: isolated Node 24.21.0 and npm 11.11.1. The machine's default Node/npm were not changed; follow README setup before installing locally. + +## Remaining manual checks + +- Final client: navigation, activity assessment, filters, sort, favorites and both storage lifetimes. +- Real devices/browser versions, portrait/landscape, 200% zoom, keyboard and screen reader. +- Real weather provider, slow/offline/retry behavior, timestamp/unit correctness and console output. +- Apache /project1/ deployment, public API CORS and production network requests. +- Native VM API/database deployment and restart; security model before public write access. +- Teammate review, issue acceptance checks and delivery evidence. diff --git a/e2e/weather.spec.ts b/e2e/weather.spec.ts new file mode 100644 index 0000000..8586ca1 --- /dev/null +++ b/e2e/weather.spec.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@playwright/test'; +import AxeBuilder from '@axe-core/playwright'; +import { weatherResponse } from '../web/src/test/fixtures'; +test('weather example loads accessibly without horizontal overflow', async ({ + page, +}) => { + let calls = 0; + await page.route('https://api.open-meteo.com/**', async (route) => { + calls++; + await route.fulfill({ json: weatherResponse }); + }); + await page.goto('./'); + await expect( + page.getByRole('heading', { name: 'Været i Trondheim' }), + ).toBeVisible(); + await expect(page.getByText('14 °C')).toBeVisible(); + expect(calls).toBe(1); + expect( + await page.evaluate( + () => document.documentElement.scrollWidth <= window.innerWidth, + ), + ).toBe(true); + expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]); +}); diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..1523ed4 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,56 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import hooks from 'eslint-plugin-react-hooks'; +import refresh from 'eslint-plugin-react-refresh'; +import a11y from 'eslint-plugin-jsx-a11y'; +import prettier from 'eslint-config-prettier'; +export default tseslint.config( + { + ignores: [ + '**/dist/**', + '**/node_modules/**', + '**/generated.ts', + 'coverage/**', + 'playwright-report/**', + 'test-results/**', + ], + }, + js.configs.recommended, + { + files: ['**/*.{ts,tsx}'], + extends: [tseslint.configs.recommendedTypeChecked], + languageOptions: { + parserOptions: { + project: [ + './web/tsconfig.app.json', + './tsconfig.tools.json', + './project2/api/tsconfig.json', + ], + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + files: ['web/src/**/*.{ts,tsx}'], + plugins: { + 'react-hooks': hooks, + 'react-refresh': refresh, + 'jsx-a11y': a11y, + }, + rules: { + ...hooks.configs.recommended.rules, + ...a11y.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'error', + { allowConstantExport: true }, + ], + }, + }, + { + files: ['**/*.mjs'], + languageOptions: { + globals: { process: 'readonly', console: 'readonly', URL: 'readonly' }, + }, + }, + prettier, +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7947645 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,9626 @@ +{ + "name": "t31-project-foundation", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "t31-project-foundation", + "workspaces": [ + "web", + "project2/api" + ], + "devDependencies": { + "@axe-core/playwright": "4.13.0", + "@eslint/js": "9.39.5", + "@graphql-codegen/core": "6.2.0", + "@graphql-codegen/typed-document-node": "7.1.0", + "@graphql-codegen/typescript": "6.1.0", + "@graphql-codegen/typescript-operations": "6.1.6", + "@graphql-codegen/typescript-resolvers": "6.1.0", + "@graphql-typed-document-node/core": "3.2.0", + "@playwright/test": "1.63.0", + "@testing-library/jest-dom": "7.0.1", + "@testing-library/react": "16.3.3", + "@testing-library/user-event": "14.6.7", + "@types/pg": "8.23.1", + "@vitest/coverage-v8": "5.0.0", + "concurrently": "10.0.5", + "drizzle-kit": "0.31.10", + "eslint": "9.39.5", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-jsx-a11y": "6.10.2", + "eslint-plugin-react-hooks": "7.1.1", + "eslint-plugin-react-refresh": "0.5.6", + "jsdom": "30.0.1", + "msw": "2.15.0", + "prettier": "3.9.6", + "tsx": "4.23.13", + "typescript": "6.0.2", + "typescript-eslint": "8.70.0", + "vitest": "5.0.0" + }, + "engines": { + "node": ">=24.6.0 <25", + "npm": ">=11 <12" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ardatan/relay-compiler": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-13.0.2.tgz", + "integrity": "sha512-VFpv9UP820SiwDUPYtq7PmD3jifzZlevkQ26bhbSzFeruSTys0eHzQCZyKg+IhgmZzwPI9AFjPe26ABNjGeIKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^8.0.0", + "immutable": "^5.1.9", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "graphql": "*" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", + "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.3.0", + "@csstools/css-color-parser": "^4.1.10", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", + "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@axe-core/playwright": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.13.0.tgz", + "integrity": "sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.13.0" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-8.0.5.tgz", + "integrity": "sha512-7NK+Lz3spQ52XsUGTxIEVU4jYN2/dIaX8sTxRFAUtYmttYZnVh3aehiihv+/Gb7+duMNHl2OrFcBQRyOHScxpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz", + "integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.13.tgz", + "integrity": "sha512-i9ZylF5QNhmNfPA9l0vHAWK4kPrbIp6g9lKgaiIFsIBz2F/WNB7OLrzlNNcCOm+h42bkaSD2v1PG+IBPHhc3ZA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@envelop/core": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@envelop/core/-/core-5.6.0.tgz", + "integrity": "sha512-cD7HNfAzJVw/0Pxneu51UAKzUGLvkctk9rr9DVJ9b7FDe4nSa9kAGMRxx145H6ooELIUMjTd2buk3PuvjJmp/A==", + "license": "MIT", + "dependencies": { + "@envelop/instrumentation": "^1.0.0", + "@envelop/types": "^5.2.1", + "@whatwg-node/promise-helpers": "^1.2.4", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@envelop/instrumentation": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@envelop/instrumentation/-/instrumentation-1.0.0.tgz", + "integrity": "sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw==", + "license": "MIT", + "dependencies": { + "@whatwg-node/promise-helpers": "^1.2.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@envelop/types": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@envelop/types/-/types-5.2.1.tgz", + "integrity": "sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==", + "license": "MIT", + "dependencies": { + "@whatwg-node/promise-helpers": "^1.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@fastify/busboy": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.2.tgz", + "integrity": "sha512-yXSS27qPExaXeuLvMRMXOLtpipzfQYNjG3FkunDWKGfMYjKuhFXko9CVzqxm8jcF+lmtS9Fd89QNdh9XDjnbNg==", + "license": "MIT" + }, + "node_modules/@graphql-codegen/core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/core/-/core-6.2.0.tgz", + "integrity": "sha512-RZadhhwYhuy2ZdIGK40vYVBMzXEFGkCC+58MUC/F2af/gKznEYNzHgmNBUBCk/BTklyUsNu0mIXmyGE4tTA0PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^7.1.0", + "@graphql-tools/schema": "^10.0.0", + "@graphql-tools/utils": "^11.2.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-codegen/plugin-helpers": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-7.3.0.tgz", + "integrity": "sha512-aaGfI/8Q8wmdO1nx1NMWGoZn/5Tl5TNOVQSI2qkGhk8h8Z8Uc6grFH0O6//HdHfqnUsiruBbe3IaK4/ah3sWcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^11.2.0", + "change-case-all": "^2.1.0", + "common-tags": "1.8.2", + "import-from": "4.0.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-codegen/schema-ast": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/schema-ast/-/schema-ast-6.1.0.tgz", + "integrity": "sha512-/xuGkM5gUNFRoaQLumKbENdX7Hc8ha49z9OXsEZY8E+46mMjqzXGF0NtCJ892cmoX7EUgI5c8T+LZqS2upx2Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^7.1.0", + "@graphql-tools/utils": "^11.2.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-codegen/typed-document-node": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-7.1.0.tgz", + "integrity": "sha512-V6H+ItyqXtYY+JQb76LAoN627Xfzpn29/ifwCFAv61iEepzNzh86sa+yZclflr0G8LDmhcVY5hpPJd3a1qbOfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^7.1.0", + "@graphql-codegen/visitor-plugin-common": "^7.2.0", + "auto-bind": "^5.0.0", + "change-case-all": "^2.1.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-codegen/typescript": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-6.1.0.tgz", + "integrity": "sha512-2Hu3111O/AwV28Ap7tNsixlmXSAJuQbQArQklx+IC/tNswpckZnCfmlcBtTJrGU1+mJXEneJXGfb2XWvKjbhlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^7.1.0", + "@graphql-codegen/schema-ast": "^6.1.0", + "@graphql-codegen/visitor-plugin-common": "^7.2.0", + "auto-bind": "^5.0.0", + "tslib": "~2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-codegen/typescript-operations": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-6.1.6.tgz", + "integrity": "sha512-qsYRkK27YbSL2doifPvldVwdwHRQtpsxXJINs22wQWGksC1Mrgn9YNaAGUZ2so46U20TKTm4SBoiNXRJE3esqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^7.1.0", + "@graphql-codegen/schema-ast": "^6.1.0", + "@graphql-codegen/visitor-plugin-common": "^7.2.5", + "auto-bind": "^5.0.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", + "graphql-sock": "^1.0.0" + }, + "peerDependenciesMeta": { + "graphql-sock": { + "optional": true + } + } + }, + "node_modules/@graphql-codegen/typescript-resolvers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-resolvers/-/typescript-resolvers-6.1.0.tgz", + "integrity": "sha512-5I8ymPQZEh2ShDaOrFcrF6DQaJQ5Z0hEIqYwMW8VboH4N8OMdyRZ3vMbiEPdfS+rfaoCf+f660+hKfrmK+JGuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^7.1.0", + "@graphql-codegen/typescript": "^6.1.0", + "@graphql-codegen/visitor-plugin-common": "^7.2.0", + "@graphql-tools/utils": "^11.2.0", + "auto-bind": "^5.0.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", + "graphql-sock": "^1.0.0" + }, + "peerDependenciesMeta": { + "graphql-sock": { + "optional": true + } + } + }, + "node_modules/@graphql-codegen/visitor-plugin-common": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-7.2.5.tgz", + "integrity": "sha512-hqOemBp0Ue+WSmk8EDcKP+lghuLC2zsI+jHzSUuoffTZnEN2qZH4VP2C9c1fVd2yy6v+YQiiObOarTMaqF4q9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-codegen/plugin-helpers": "^7.1.0", + "@graphql-tools/optimize": "^2.0.0", + "@graphql-tools/relay-operation-optimizer": "^7.1.1", + "@graphql-tools/utils": "^11.2.0", + "auto-bind": "^5.0.0", + "change-case-all": "^2.1.0", + "dependency-graph": "^1.0.0", + "graphql-tag": "^2.11.0", + "parse-filepath": "^1.0.2", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-2.0.1.tgz", + "integrity": "sha512-rvlOaoTUXUUHlu7090e94566SeBnU9PV6xMztIt2s8StXy5HbP0p+tYSfg3N/gCdEzJ78ZQLjXJz4UFgWGZ/7A==", + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^12.0.1", + "@graphql-typed-document-node/core": "^3.2.0", + "@repeaterjs/repeater": "^3.1.0", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/promise-helpers": "^1.0.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor/node_modules/@graphql-tools/utils": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-12.0.1.tgz", + "integrity": "sha512-8YC6jn4xYDS6YTY6xDAgQAs/nGgvzRaIGHS8GeSfVItQzNK7Ms24CQtE3EJY+amvR+tBnhRSX0N83aGj+V/RIg==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/merge": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.2.4.tgz", + "integrity": "sha512-vV+8uWNWn0+OsqT0r22lZuoT0cTe6fBqBtpLHre2rriLjI/ZTrsOHmabLPTUOyzgFnRrS2PzFSYGL3zKL1Wj4Q==", + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^12.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/merge/node_modules/@graphql-tools/utils": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-12.0.1.tgz", + "integrity": "sha512-8YC6jn4xYDS6YTY6xDAgQAs/nGgvzRaIGHS8GeSfVItQzNK7Ms24CQtE3EJY+amvR+tBnhRSX0N83aGj+V/RIg==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/optimize": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/optimize/-/optimize-2.0.1.tgz", + "integrity": "sha512-I63H75wg7SD/+iPj+QdvZgvul0dxxf/el18Yiry5zmwKeqRvzcm6l2KUYRNgMR/ydytp0VuHw3XeyGhPWGigAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/relay-operation-optimizer": { + "version": "7.1.10", + "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.1.10.tgz", + "integrity": "sha512-dCZAyUQkrrC0R3SOIRMUXaCct/uMOJeNmI028yJH6kd6lZzjIVPaX/6NLl6opg1pYdUkjOQ+SiyfTKNmuS71gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ardatan/relay-compiler": "^13.0.2", + "@graphql-tools/utils": "^12.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/relay-operation-optimizer/node_modules/@graphql-tools/utils": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-12.0.1.tgz", + "integrity": "sha512-8YC6jn4xYDS6YTY6xDAgQAs/nGgvzRaIGHS8GeSfVItQzNK7Ms24CQtE3EJY+amvR+tBnhRSX0N83aGj+V/RIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/schema": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.1.1.tgz", + "integrity": "sha512-24jJghRxEW+SG1lbJ45Zg9HEZ8ZKS923ihbFjOicspFLpryxJkWp6gZJ7D8tCVaSAhli4x1+1wDPAfhT8mLqxg==", + "license": "MIT", + "dependencies": { + "@graphql-tools/merge": "^9.2.4", + "@graphql-tools/utils": "^12.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/schema/node_modules/@graphql-tools/utils": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-12.0.1.tgz", + "integrity": "sha512-8YC6jn4xYDS6YTY6xDAgQAs/nGgvzRaIGHS8GeSfVItQzNK7Ms24CQtE3EJY+amvR+tBnhRSX0N83aGj+V/RIg==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/utils": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.2.2.tgz", + "integrity": "sha512-Do2A/t6ayqOma8m7PvpYMsCBswL+NB35BQSng4GWSBqafL2/BnfAZy/NSENPwV721Rm2fG0BHETFC0+FJJZmLw==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-yoga/logger": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@graphql-yoga/logger/-/logger-2.0.1.tgz", + "integrity": "sha512-Nv0BoDGLMg9QBKy9cIswQ3/6aKaKjlTh87x3GiBg2Z4RrjyrM48DvOOK0pJh1C1At+b0mUIM67cwZcFTDLN4sA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@graphql-yoga/subscription": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@graphql-yoga/subscription/-/subscription-5.0.5.tgz", + "integrity": "sha512-oCMWOqFs6QV96/NZRt/ZhTQvzjkGB4YohBOpKM4jH/lDT4qb7Lex/aGCxpi/JD9njw3zBBtMqxbaC22+tFHVvw==", + "license": "MIT", + "dependencies": { + "@graphql-yoga/typed-event-target": "^3.0.2", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/events": "^0.1.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@graphql-yoga/typed-event-target": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@graphql-yoga/typed-event-target/-/typed-event-target-3.0.2.tgz", + "integrity": "sha512-ZpJxMqB+Qfe3rp6uszCQoag4nSw42icURnBRfFYSOmTgEeOe4rD0vYlbA8spvCu2TlCesNTlEN9BLWtQqLxabA==", + "license": "MIT", + "dependencies": { + "@repeaterjs/repeater": "^3.0.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.8.tgz", + "integrity": "sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.2.tgz", + "integrity": "sha512-Xvr/0HggjddPtGppuqVmxhTw+Hr8PvsZ/k0HmOEaAqQEt80OITNkFWnsdNmyT0/eM4Ab+iJLx2R8rctlEyfSVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", + "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.9.tgz", + "integrity": "sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/type": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.1.1.tgz", + "integrity": "sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.41.9", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.9.tgz", + "integrity": "sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mswjs/interceptors/node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/deferred-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-3.0.0.tgz", + "integrity": "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.149.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", + "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@playwright/test": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@repeaterjs/repeater": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.1.0.tgz", + "integrity": "sha512-TaoVksZRSx2KWYYpyLQtMQXXeS98VsgZImzW65xmiVgbYhXLk+aEsmzPLirqVuE4/XuUapH2iMtxUzaBNDzdSQ==", + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", + "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", + "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", + "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", + "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", + "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", + "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", + "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", + "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", + "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", + "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", + "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", + "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", + "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", + "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", + "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@t31/project2-api": { + "resolved": "project2/api", + "link": true + }, + "node_modules/@t31/web": { + "resolved": "web", + "link": true + }, + "node_modules/@tanstack/query-core": { + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.102.8.tgz", + "integrity": "sha512-ZNjkJ33CqvPNec/6lZBnHqLc3EVGPZ9ySLhYahU9TcuRFdmwXewuj0c4hwSWcGHqEUwcSrKeZ+oGcvPBqXcQcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.102.8.tgz", + "integrity": "sha512-TYBea4OuXWD7MhaSHq069TWbFe7rcwWN6kzT7JF0OKi1K6c1gTv2IzD6A6ExJsCMozdkqBWeuIUZmu4KQg0O5A==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.102.8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", + "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/react/node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.7", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.7.tgz", + "integrity": "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz", + "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/set-cookie-parser": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz", + "integrity": "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz", + "integrity": "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/type-utils": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.70.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", + "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.0.tgz", + "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz", + "integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.70.0", + "@typescript-eslint/types": "^8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz", + "integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz", + "integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.0.tgz", + "integrity": "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz", + "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz", + "integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.70.0", + "@typescript-eslint/tsconfig-utils": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.0.tgz", + "integrity": "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz", + "integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-5.0.0.tgz", + "integrity": "sha512-toMg6PZGCIa/lQNCDoASrfb1ly4hsUKXFtFYC9kD4t78o5Y6LyNJU7AENt8eHPr3quYdxaxK7hj2mnbFfUk9NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/istanbul-lib-coverage": "^1.0.0", + "@vitest/istanbul-lib-report": "^1.0.0", + "ast-v8-to-istanbul": "^1.0.5", + "magicast": "^0.5.4", + "obug": "^2.1.4", + "std-env": "^4.2.0", + "tinyrainbow": "^3.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "5.0.0", + "vitest": "5.0.0" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/istanbul-lib-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.1.tgz", + "integrity": "sha512-k3DJZ8LhMBK9NS4SclF1ASD3OgXEWDorbIcPTRDK0/Zae6fRvu+fJRxtFdLfHsa9Y24beCdPnoNZ4LviTNstfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@vitest/istanbul-lib-report": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-report/-/istanbul-lib-report-1.0.1.tgz", + "integrity": "sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/istanbul-lib-coverage": "1.0.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.0.tgz", + "integrity": "sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.0", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.0.tgz", + "integrity": "sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@whatwg-node/disposablestack": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz", + "integrity": "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==", + "license": "MIT", + "dependencies": { + "@whatwg-node/promise-helpers": "^1.0.0", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@whatwg-node/events": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.1.2.tgz", + "integrity": "sha512-ApcWxkrs1WmEMS2CaLLFUEem/49erT3sxIVjpzU5f6zmVcnijtDSrhoK2zVobOIikZJdH63jdAXOrvjf6eOUNQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@whatwg-node/fetch": { + "version": "0.10.13", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.13.tgz", + "integrity": "sha512-b4PhJ+zYj4357zwk4TTuF2nEe0vVtOrwdsrNo5hL+u1ojXNhh1FgJ6pg1jzDlwlT4oBdzfSwaBwMCtFCsIWg8Q==", + "license": "MIT", + "dependencies": { + "@whatwg-node/node-fetch": "^0.8.3", + "urlpattern-polyfill": "^10.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@whatwg-node/node-fetch": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.8.6.tgz", + "integrity": "sha512-BDMdYFcerLQkwA2RTldxOqRCs6ZQD1S7UgP3pUdGUkcbgTrP/V5ko77ZkCww9DHmC4lpoYuwigGfQYj285gMvA==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^3.1.1", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/promise-helpers": "^1.3.2", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@whatwg-node/promise-helpers": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@whatwg-node/promise-helpers/-/promise-helpers-1.3.2.tgz", + "integrity": "sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@whatwg-node/server": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@whatwg-node/server/-/server-0.11.0.tgz", + "integrity": "sha512-VSdkwnJRr8Yv9UgB2aXB3VUPWwd6Oqnn0hycFwhg9pZgWxJXb7JmhsiXe9tmpMwjHFxli12PGcz9aI63YYloGQ==", + "license": "MIT", + "dependencies": { + "@envelop/instrumentation": "^1.0.0", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/fetch": "^0.10.13", + "@whatwg-node/promise-helpers": "^1.3.2", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.2.0.tgz", + "integrity": "sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "es-shim-unscopables": "^1.1.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.6.tgz", + "integrity": "sha512-fvpl29helSO2w/z7utIbrkNXILdrLwDwAMH2I/zPKlGf5244+gf+B4cyS1sANcrPY2h+hWCGSgC8N61s/+AF9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", + "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/babel-plugin-react-compiler": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", + "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.22", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.22.tgz", + "integrity": "sha512-pWc4w51fBFd7mav43/zKRC+RI6f4yfzQoVlfvE8dECePyfkn1bzLp01Fj0QACcyCZyFhiEMyD2qScfKRWgWibA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/change-case-all": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/change-case-all/-/change-case-all-2.1.0.tgz", + "integrity": "sha512-v6b0WWWkZUMHVuYk82l+WROgkUm4qEN2w5hKRNWtEOYwWqUGoi8C6xH0l1RLF1EoWqDFK6MFclmN3od6ws3/uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "change-case": "^5.2.0", + "sponge-case": "^2.0.2", + "swap-case": "^3.0.2", + "title-case": "^3.0.3" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.5.tgz", + "integrity": "sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "5.6.2", + "rxjs": "7.8.2", + "shell-quote": "1.9.0", + "supports-color": "10.2.2", + "tree-kill": "1.2.2", + "yargs": "18.0.0" + }, + "bin": { + "conc": "dist/bin/index.js", + "concurrently": "dist/bin/index.js" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-inspect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cross-inspect/-/cross-inspect-1.0.1.tgz", + "integrity": "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dependency-graph": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/drizzle-kit": { + "version": "0.31.10", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", + "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "tsx": "^4.21.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.427", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.427.tgz", + "integrity": "sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.6.tgz", + "integrity": "sha512-uZnh24On2bk478AkaDAKcpRiYhS3qFSaSNvpXxV6/Ek/BBDICkWGG7MnBSfjnIlCVBei5vsLlIeQYxbF22+Udg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphql": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-17.0.2.tgz", + "integrity": "sha512-FRWbddMxfkjiB7z+aQDWIR+E34xo9I8c9mtK2RPv8PmMzKRvrdsreHL/Ui/TmwHJfhHChEtsFPyMHKI+xuarQQ==", + "license": "MIT", + "engines": { + "node": "^22.0.0 || ^24.0.0 || ^25.0.0 || >=26.0.0" + } + }, + "node_modules/graphql-tag": { + "version": "2.12.7", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.7.tgz", + "integrity": "sha512-xnE/NFzy+0eIesvAsREJZ284zTl/wYuBAvpsFSDhRGRdRHdnE90M21Q3xAWyYInb0J756c6x0pIQ62+vtvOs1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-yoga": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/graphql-yoga/-/graphql-yoga-5.22.0.tgz", + "integrity": "sha512-RTMS9WfDJQdYT1VRwDQ34Q9zFvuD3PZXBEDedNdUuoeNsJ6jZBwPo5PhdExzkR+s5iwf8MoiGJOS5Byt/PNAZg==", + "license": "MIT", + "dependencies": { + "@envelop/core": "^5.6.0", + "@envelop/instrumentation": "^1.0.0", + "@graphql-tools/executor": "^2.0.0", + "@graphql-tools/schema": "^10.0.11", + "@graphql-tools/utils": "^11.2.0", + "@graphql-yoga/logger": "^2.0.1", + "@graphql-yoga/subscription": "^5.0.5", + "@whatwg-node/fetch": "^0.10.6", + "@whatwg-node/promise-helpers": "^1.3.2", + "@whatwg-node/server": "^0.11.0", + "lru-cache": "^10.0.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "graphql": "^15.2.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-yoga/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/headers-polyfill": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-5.0.1.tgz", + "integrity": "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/set-cookie-parser": "^2.4.10", + "set-cookie-parser": "^3.0.1" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hono": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz", + "integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-4.0.0.tgz", + "integrity": "sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", + "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@exodus/bytes": "^1.15.1", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.2", + "undici": "^8.9.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.3.1.tgz", + "integrity": "sha512-rm91zr2Ou+XueDTohjQQjdQEcYM6zVi8KVUCG8Ec3vHwUEKrhSdCNyfuIywkA6hcCAteIn0ZOtAHA6eGpiX+Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/magicast": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msw": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.15.0.tgz", + "integrity": "sha512-2wQAmKkQKxRuXvYJxVhPGG0wZNBQyD06oJvxqw90XqLvptdqxdlHrFUfEteKkpaNORX3Xzc+HtEl/q0nfmN2wQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^6.0.11", + "@mswjs/interceptors": "^0.41.3", + "@open-draft/deferred-promise": "^3.0.0", + "@types/statuses": "^2.0.6", + "cookie": "^1.1.1", + "graphql": "^16.13.2", + "headers-polyfill": "^5.0.1", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.11.11", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.1", + "type-fest": "^5.5.0", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/msw/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/msw/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/msw/node_modules/graphql": { + "version": "16.14.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", + "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/msw/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/msw/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/msw/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/msw/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/msw/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rettime": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.11.tgz", + "integrity": "sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", + "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.149.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.8", + "@rolldown/binding-android-arm64": "1.2.8", + "@rolldown/binding-darwin-arm64": "1.2.8", + "@rolldown/binding-darwin-x64": "1.2.8", + "@rolldown/binding-freebsd-x64": "1.2.8", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", + "@rolldown/binding-linux-arm64-gnu": "1.2.8", + "@rolldown/binding-linux-arm64-musl": "1.2.8", + "@rolldown/binding-linux-ppc64-gnu": "1.2.8", + "@rolldown/binding-linux-s390x-gnu": "1.2.8", + "@rolldown/binding-linux-x64-gnu": "1.2.8", + "@rolldown/binding-linux-x64-musl": "1.2.8", + "@rolldown/binding-openharmony-arm64": "1.2.8", + "@rolldown/binding-win32-arm64-msvc": "1.2.8", + "@rolldown/binding-win32-x64-msvc": "1.2.8" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", + "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sponge-case": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/sponge-case/-/sponge-case-2.0.3.tgz", + "integrity": "sha512-i4h9ZGRfxV6Xw3mpZSFOfbXjf0cQcYmssGWutgNIfFZ2VM+YIWfD71N/kjjwK6X/AAHzBr+rciEcn/L34S8TGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/swap-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/swap-case/-/swap-case-3.0.3.tgz", + "integrity": "sha512-6p4op8wE9CQv7uDFzulI6YXUw4lD9n4oQierdbFThEKVWVQcbQcUjdP27W8XE7V4QnWmnq9jueSHceyyQnqQVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tinybench": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz", + "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/title-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", + "integrity": "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/tldts": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.12.tgz", + "integrity": "sha512-WylhSDKVeYnWXL3a+vKTaOxjnOeEGw938hImY8zoRWJjRRK/Jp1K+IihBzIONpUmW4e3WmXT6q5FW6vlESVZCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.12" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.12.tgz", + "integrity": "sha512-nYNzS2WRf4QJmjzFFgAxLOBjyBxAGRbCy9PVBPaglcYyYajh40VBn+v5Ngr96ZMc7oM0+aCJdtQnNejvdBnXMQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.9.0.tgz", + "integrity": "sha512-yANm3Jr3GiJ1qgJlxGAVxTOIcEOk1rhQHamlXtnrCK7EHP4HeM9OGxtMg/W7HFdrVzw/ZWJKGVIJusVH85sLtw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.0.tgz", + "integrity": "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.70.0", + "@typescript-eslint/parser": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/undici": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz", + "integrity": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.0.tgz", + "integrity": "sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.0", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "6.1.4", + "tinyexec": "1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.0", + "@vitest/browser-preview": "5.0.0", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.0", + "@vitest/coverage-v8": "5.0.0", + "@vitest/ui": "5.0.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.1.tgz", + "integrity": "sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.2.tgz", + "integrity": "sha512-lh5RCAGFa1Cm2hjtNwLQhSs/AsqdWnTQaBER9fEwN/88pSh7KOtJavtBx/0VlkN/uFd61SwYmljLMDAsHlvzBQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "project2/api": { + "name": "@t31/project2-api", + "dependencies": { + "@hono/node-server": "2.1.1", + "drizzle-orm": "0.45.2", + "graphql": "17.0.2", + "graphql-yoga": "5.22.0", + "hono": "4.13.7", + "pg": "8.23.0", + "zod": "4.6.2" + } + }, + "web": { + "name": "@t31/web", + "version": "0.0.0", + "dependencies": { + "@tanstack/react-query": "5.102.8", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "zod": "4.6.2" + }, + "devDependencies": { + "@babel/core": "7.29.7", + "@rolldown/plugin-babel": "0.2.4", + "@types/babel__core": "7.20.5", + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "@vitejs/plugin-react": "^6.1.1", + "babel-plugin-react-compiler": "1.0.0", + "typescript": "~6.0.2", + "vite": "^8.3.0" + } + }, + "web/node_modules/@rolldown/plugin-babel": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/plugin-babel/-/plugin-babel-0.2.4.tgz", + "integrity": "sha512-ygmm2j/mN+aCvstHo0x3IOu4uc6LqXT9SJ3q4kpfG/+Yhfz0B+E47FITcC0FljZADwUFARP6E2kiE+Lc26D8wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^4.0.7" + }, + "engines": { + "node": ">=22.12.0 || ^24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.29.0 || ^8.0.0-rc.1", + "@babel/plugin-transform-runtime": "^7.29.0 || ^8.0.0-rc.1", + "@babel/runtime": "^7.27.0 || ^8.0.0-rc.1", + "rolldown": "^1.0.0-rc.5", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@babel/plugin-transform-runtime": { + "optional": true + }, + "@babel/runtime": { + "optional": true + }, + "vite": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..24f6b5c --- /dev/null +++ b/package.json @@ -0,0 +1,79 @@ +{ + "name": "t31-project-foundation", + "private": true, + "type": "module", + "workspaces": [ + "web", + "project2/api" + ], + "engines": { + "node": ">=24.6.0 <25", + "npm": ">=11 <12" + }, + "packageManager": "npm@11.11.1", + "scripts": { + "setup": "npm ci && npm run env:init && npm run types:api", + "env:init": "node scripts/env-init.mjs", + "dev": "npm run dev --workspace @t31/web", + "dev:web": "npm run dev", + "dev:api": "npm run dev --workspace @t31/project2-api", + "dev:all": "concurrently -k -n web,api 'npm run dev' 'npm run dev:api'", + "build": "npm run build --workspace @t31/web", + "build:api": "npm run build --workspace @t31/project2-api", + "preview": "npm run preview --workspace @t31/web", + "start:api": "npm run start --workspace @t31/project2-api", + "typecheck": "tsc -p web/tsconfig.app.json && tsc -p tsconfig.tools.json && tsc -p project2/api/tsconfig.json --noEmit", + "types:api": "tsx scripts/generate-api.ts", + "types:check": "tsx scripts/generate-api.ts --check", + "types:db": "tsc -p project2/api/tsconfig.json --noEmit", + "db:generate": "drizzle-kit generate --config project2/api/drizzle.config.ts", + "db:migrate": "tsx --env-file-if-exists=.env scripts/database.ts migrate", + "db:seed": "tsx --env-file-if-exists=.env scripts/database.ts seed", + "db:reset": "tsx --env-file-if-exists=.env scripts/database.ts reset", + "db:check": "drizzle-kit check --config project2/api/drizzle.config.ts", + "lint": "eslint . --max-warnings 0", + "lint:fix": "npm run lint -- --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:e2e": "playwright test", + "test:e2e:install": "playwright install chromium firefox webkit", + "test:db": "tsx --env-file-if-exists=.env scripts/database-smoke.ts", + "check": "npm run format:check && npm run lint && npm run types:check && npm run typecheck && npm run db:check && npm run test && npm run build && npm run build:api", + "docker:up": "docker compose up --build -d", + "docker:down": "docker compose down", + "docker:logs": "docker compose logs -f web" + }, + "devDependencies": { + "@axe-core/playwright": "4.13.0", + "@eslint/js": "9.39.5", + "@graphql-codegen/core": "6.2.0", + "@graphql-codegen/typed-document-node": "7.1.0", + "@graphql-codegen/typescript": "6.1.0", + "@graphql-codegen/typescript-operations": "6.1.6", + "@graphql-codegen/typescript-resolvers": "6.1.0", + "@graphql-typed-document-node/core": "3.2.0", + "@playwright/test": "1.63.0", + "@testing-library/jest-dom": "7.0.1", + "@testing-library/react": "16.3.3", + "@testing-library/user-event": "14.6.7", + "@types/pg": "8.23.1", + "@vitest/coverage-v8": "5.0.0", + "concurrently": "10.0.5", + "drizzle-kit": "0.31.10", + "eslint": "9.39.5", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-jsx-a11y": "6.10.2", + "eslint-plugin-react-hooks": "7.1.1", + "eslint-plugin-react-refresh": "0.5.6", + "jsdom": "30.0.1", + "msw": "2.15.0", + "prettier": "3.9.6", + "tsx": "4.23.13", + "typescript": "6.0.2", + "typescript-eslint": "8.70.0", + "vitest": "5.0.0" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..9503f92 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,20 @@ +import { defineConfig, devices } from '@playwright/test'; +export default defineConfig({ + testDir: './e2e', + use: { + baseURL: 'http://127.0.0.1:4173/project1/', + trace: 'retain-on-failure', + }, + webServer: { + command: 'npm run build && npm run preview', + url: 'http://127.0.0.1:4173/project1/', + reuseExistingServer: false, + }, + projects: [ + { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, + { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, + { name: 'webkit', use: { ...devices['Desktop Safari'] } }, + { name: 'mobile', use: { ...devices['iPhone 13'] } }, + { name: 'mobile-landscape', use: { ...devices['iPhone 13 landscape'] } }, + ], +}); diff --git a/project2/api/drizzle.config.ts b/project2/api/drizzle.config.ts new file mode 100644 index 0000000..168dffa --- /dev/null +++ b/project2/api/drizzle.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'drizzle-kit'; +export default defineConfig({ + dialect: 'postgresql', + schema: './project2/api/src/db/schema.ts', + out: './project2/api/migrations', + schemaFilter: ['project2'], +}); diff --git a/project2/api/migrations/0000_flippant_barracuda.sql b/project2/api/migrations/0000_flippant_barracuda.sql new file mode 100644 index 0000000..2647515 --- /dev/null +++ b/project2/api/migrations/0000_flippant_barracuda.sql @@ -0,0 +1,8 @@ +CREATE SCHEMA "project2"; +--> statement-breakpoint +CREATE TABLE "project2"."notes" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "body" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "note_body_length" CHECK (length(trim("project2"."notes"."body")) between 1 and 500) +); diff --git a/project2/api/migrations/meta/0000_snapshot.json b/project2/api/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..f1dd522 --- /dev/null +++ b/project2/api/migrations/meta/0000_snapshot.json @@ -0,0 +1,59 @@ +{ + "id": "73d4b143-ef5a-4bcd-b106-5ee84734b88b", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "project2.notes": { + "name": "notes", + "schema": "project2", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "note_body_length": { + "name": "note_body_length", + "value": "length(trim(\"project2\".\"notes\".\"body\")) between 1 and 500" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": { + "project2": "project2" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/project2/api/migrations/meta/_journal.json b/project2/api/migrations/meta/_journal.json new file mode 100644 index 0000000..72697ef --- /dev/null +++ b/project2/api/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1789118025220, + "tag": "0000_flippant_barracuda", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/project2/api/package.json b/project2/api/package.json new file mode 100644 index 0000000..8ae7a86 --- /dev/null +++ b/project2/api/package.json @@ -0,0 +1,19 @@ +{ + "name": "@t31/project2-api", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx --env-file-if-exists=../../.env watch src/server.ts", + "build": "tsc -p tsconfig.build.json && node ../../scripts/copy-schema.mjs", + "start": "node --env-file-if-exists=../../.env dist/server.js" + }, + "dependencies": { + "@hono/node-server": "2.1.1", + "drizzle-orm": "0.45.2", + "graphql": "17.0.2", + "graphql-yoga": "5.22.0", + "hono": "4.13.7", + "pg": "8.23.0", + "zod": "4.6.2" + } +} diff --git a/project2/api/src/app.test.ts b/project2/api/src/app.test.ts new file mode 100644 index 0000000..45ca604 --- /dev/null +++ b/project2/api/src/app.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it, vi } from 'vitest'; +import { createApp } from './app.js'; +import type { Repository } from './db/repository.js'; +const note = { + id: '00000000-0000-4000-8000-000000000001', + body: 'Hello', + createdAt: '2026-09-11T00:00:00Z', +}; +function setup() { + const repository = { + ping: vi.fn().mockResolvedValue(undefined), + listNotes: vi.fn().mockResolvedValue([note]), + createNote: vi + .fn() + .mockImplementation((body) => Promise.resolve({ ...note, body })), + close: vi.fn().mockResolvedValue(undefined), + } satisfies Repository; + const app = createApp(repository, ['http://localhost:5173']); + const query = (query: string) => + app.request('/graphql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query }), + }); + return { repository, app, query }; +} +describe('GraphQL foundation', () => { + it('executes typed queries and mutations through the repository boundary', async () => { + const { query, repository } = setup(); + const response = await query('{ notes(first: 5) { id body } }'); + expect(await response.json()).toEqual({ + data: { notes: [{ id: note.id, body: 'Hello' }] }, + }); + expect(repository.listNotes).toHaveBeenCalledWith(5); + const created = await query( + 'mutation { createNote(body: " Hello ") { body } }', + ); + expect(await created.json()).toEqual({ + data: { createNote: { body: 'Hello' } }, + }); + expect(repository.createNote).toHaveBeenCalledWith('Hello'); + }); + it('rejects empty notes and unbounded list requests', async () => { + const { query, repository } = setup(); + expect( + await (await query('mutation { createNote(body: " ") { id } }')).json(), + ).toHaveProperty('errors'); + expect( + await (await query('{ notes(first: 1000) { id } }')).json(), + ).toHaveProperty('errors'); + expect(repository.createNote).not.toHaveBeenCalled(); + expect(repository.listNotes).not.toHaveBeenCalled(); + }); + it('allows only configured CORS origins and reports unavailable database readiness', async () => { + const { app, repository } = setup(); + const allowed = await app.request('/health', { + headers: { Origin: 'http://localhost:5173' }, + }); + expect(allowed.headers.get('Access-Control-Allow-Origin')).toBe( + 'http://localhost:5173', + ); + const denied = await app.request('/health', { + headers: { Origin: 'https://other.example' }, + }); + expect(denied.headers.get('Access-Control-Allow-Origin')).toBeNull(); + vi.mocked(repository.ping).mockRejectedValue(new Error('offline')); + expect((await app.request('/ready')).status).toBe(503); + }); +}); diff --git a/project2/api/src/app.ts b/project2/api/src/app.ts new file mode 100644 index 0000000..dff7065 --- /dev/null +++ b/project2/api/src/app.ts @@ -0,0 +1,80 @@ +import { readFileSync } from 'node:fs'; +import { Hono } from 'hono'; +import { bodyLimit } from 'hono/body-limit'; +import { cors } from 'hono/cors'; +import { GraphQLError } from 'graphql'; +import { createSchema, createYoga } from 'graphql-yoga'; +import { z } from 'zod'; +import type { ApiContext, Repository } from './db/repository.js'; +import type { Resolvers } from './generated.js'; + +export function createApp( + repository: Repository, + origins: string[], + production = false, +) { + const resolvers: Resolvers = { + Query: { + health: () => 'ok', + notes: (_parent, { first }, { repository: db }) => { + const limit = z.number().int().min(1).max(50).safeParse(first); + if (!limit.success) + throw new GraphQLError('first must be between 1 and 50', { + extensions: { code: 'BAD_USER_INPUT' }, + }); + return db.listNotes(limit.data); + }, + }, + Mutation: { + createNote: (_parent, { body }, { repository: db }) => { + const input = z.string().trim().min(1).max(500).safeParse(body); + if (!input.success) + throw new GraphQLError('A note must contain 1–500 characters', { + extensions: { code: 'BAD_USER_INPUT' }, + }); + return db.createNote(input.data); + }, + }, + }; + const yoga = createYoga({ + schema: createSchema({ + typeDefs: readFileSync( + new URL('./schema.graphql', import.meta.url), + 'utf8', + ), + resolvers, + }), + context: { repository }, + graphqlEndpoint: '/graphql', + graphiql: !production, + cors: false, + maskedErrors: true, + logging: false, + }); + const app = new Hono(); + app.use( + '*', + cors({ + origin: (origin) => (origins.includes(origin) ? origin : ''), + allowMethods: ['GET', 'POST', 'OPTIONS'], + allowHeaders: ['Content-Type'], + credentials: false, + }), + ); + app.use('*', bodyLimit({ maxSize: 16 * 1024 })); + app.get('/health', (c) => c.json({ status: 'ok' })); + app.get('/ready', async (c) => { + try { + await repository.ping(); + return c.json({ status: 'ready' }); + } catch { + return c.json({ status: 'unavailable' }, 503); + } + }); + app.on(['GET', 'POST'], '/graphql', (c) => yoga.fetch(c.req.raw)); + app.onError((_error, c) => { + console.error('Unexpected API request failure'); + return c.json({ error: 'Internal server error' }, 500); + }); + return app; +} diff --git a/project2/api/src/db/repository.ts b/project2/api/src/db/repository.ts new file mode 100644 index 0000000..9487e36 --- /dev/null +++ b/project2/api/src/db/repository.ts @@ -0,0 +1,39 @@ +import { desc } from 'drizzle-orm'; +import { drizzle } from 'drizzle-orm/node-postgres'; +import { Pool } from 'pg'; +import { notes } from './schema.js'; +export function createRepository(connectionString: string) { + const pool = new Pool({ + connectionString, + max: 5, + connectionTimeoutMillis: 5000, + idleTimeoutMillis: 30000, + statement_timeout: 5000, + }); + pool.on('error', () => { + console.error('Idle database connection failed'); + }); + const db = drizzle(pool); + return { + async ping() { + await pool.query('select 1'); + }, + async listNotes(first: number) { + return await db + .select() + .from(notes) + .orderBy(desc(notes.createdAt), desc(notes.id)) + .limit(first); + }, + async createNote(body: string) { + const [note] = await db.insert(notes).values({ body }).returning(); + if (!note) throw new Error('Insert did not return a note'); + return note; + }, + close() { + return pool.end(); + }, + }; +} +export type Repository = ReturnType; +export type ApiContext = { repository: Repository }; diff --git a/project2/api/src/db/schema.ts b/project2/api/src/db/schema.ts new file mode 100644 index 0000000..03fdd8b --- /dev/null +++ b/project2/api/src/db/schema.ts @@ -0,0 +1,21 @@ +import { sql } from 'drizzle-orm'; +import { check, pgSchema, text, timestamp, uuid } from 'drizzle-orm/pg-core'; +export const project2 = pgSchema('project2'); +export const notes = project2.table( + 'notes', + { + id: uuid('id').primaryKey().defaultRandom(), + body: text('body').notNull(), + createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) + .notNull() + .defaultNow(), + }, + (table) => [ + check( + 'note_body_length', + sql`length(trim(${table.body})) between 1 and 500`, + ), + ], +); +export type NoteRow = typeof notes.$inferSelect; +export type NewNote = typeof notes.$inferInsert; diff --git a/project2/api/src/env.ts b/project2/api/src/env.ts new file mode 100644 index 0000000..ce26ec8 --- /dev/null +++ b/project2/api/src/env.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; +export function parseEnv(input: NodeJS.ProcessEnv) { + return z + .object({ + DATABASE_URL: z + .url() + .refine((value) => + ['postgres:', 'postgresql:'].includes(new URL(value).protocol), + ), + API_HOST: z.enum(['127.0.0.1', '0.0.0.0']).default('127.0.0.1'), + API_PORT: z.coerce.number().int().min(1).max(65535).default(3001), + CORS_ORIGINS: z + .string() + .default('http://localhost:5173') + .transform((value) => + value.split(',').map((origin) => z.url().parse(origin.trim())), + ), + NODE_ENV: z + .enum(['development', 'test', 'production']) + .default('development'), + }) + .parse(input); +} diff --git a/project2/api/src/generated.ts b/project2/api/src/generated.ts new file mode 100644 index 0000000..8a69bfa --- /dev/null +++ b/project2/api/src/generated.ts @@ -0,0 +1,226 @@ +// Generated by npm run types:api. Do not edit. +import type { GraphQLResolveInfo } from 'graphql'; +import type { ApiContext } from './db/repository.js'; +export type Maybe = T | null; +export type InputMaybe = Maybe; +export type RequireFields = Omit & { + [P in K]-?: NonNullable; +}; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string }; + String: { input: string; output: string }; + Boolean: { input: boolean; output: boolean }; + Int: { input: number; output: number }; + Float: { input: number; output: number }; +}; + +/** Development foundation only; catalogue operations belong in the Project 2 repository. */ +export type Query = { + __typename?: 'Query'; + health: Scalars['String']['output']; + notes: Array; +}; + +/** Development foundation only; catalogue operations belong in the Project 2 repository. */ +export type QueryNotesArgs = { + first?: Scalars['Int']['input']; +}; + +export type Mutation = { + __typename?: 'Mutation'; + createNote: Note; +}; + +export type MutationCreateNoteArgs = { + body: Scalars['String']['input']; +}; + +export type Note = { + __typename?: 'Note'; + id: Scalars['ID']['output']; + body: Scalars['String']['output']; + createdAt: Scalars['String']['output']; +}; + +export type ResolverTypeWrapper = Promise | T; + +export type ResolverWithResolve = { + resolve: ResolverFn; +}; +export type Resolver< + TResult, + TParent = Record, + TContext = Record, + TArgs = Record, +> = + | ResolverFn + | ResolverWithResolve; + +export type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo, +) => Promise | TResult; + +export type SubscriptionSubscribeFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo, +) => AsyncIterable | Promise>; + +export type SubscriptionResolveFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo, +) => TResult | Promise; + +export interface SubscriptionSubscriberObject< + TResult, + TKey extends string, + TParent, + TContext, + TArgs, +> { + subscribe: SubscriptionSubscribeFn< + { [key in TKey]: TResult }, + TParent, + TContext, + TArgs + >; + resolve?: SubscriptionResolveFn< + TResult, + { [key in TKey]: TResult }, + TContext, + TArgs + >; +} + +export interface SubscriptionResolverObject { + subscribe: SubscriptionSubscribeFn; + resolve: SubscriptionResolveFn; +} + +export type SubscriptionObject< + TResult, + TKey extends string, + TParent, + TContext, + TArgs, +> = + | SubscriptionSubscriberObject + | SubscriptionResolverObject; + +export type SubscriptionResolver< + TResult, + TKey extends string, + TParent = Record, + TContext = Record, + TArgs = Record, +> = + | (( + ...args: any[] + ) => SubscriptionObject) + | SubscriptionObject; + +export type TypeResolveFn< + TTypes, + TParent = Record, + TContext = Record, +> = ( + parent: TParent, + context: TContext, + info: GraphQLResolveInfo, +) => Maybe | Promise>; + +export type IsTypeOfResolverFn< + T = Record, + TContext = Record, +> = ( + obj: T, + context: TContext, + info: GraphQLResolveInfo, +) => boolean | Promise; + +export type NextResolverFn = () => Promise; + +export type DirectiveResolverFn< + TResult = Record, + TParent = Record, + TContext = Record, + TArgs = Record, +> = ( + next: NextResolverFn, + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo, +) => TResult | Promise; + +/** Mapping between all available schema types and the resolvers types */ +export type ResolversTypes = { + Query: ResolverTypeWrapper>; + String: ResolverTypeWrapper; + Int: ResolverTypeWrapper; + Mutation: ResolverTypeWrapper>; + Note: ResolverTypeWrapper; + ID: ResolverTypeWrapper; + Boolean: ResolverTypeWrapper; +}; + +/** Mapping between all available schema types and the resolvers parents */ +export type ResolversParentTypes = { + Query: Record; + String: Scalars['String']['output']; + Int: Scalars['Int']['output']; + Mutation: Record; + Note: Note; + ID: Scalars['ID']['output']; + Boolean: Scalars['Boolean']['output']; +}; + +export type QueryResolvers< + ContextType = ApiContext, + ParentType extends ResolversParentTypes['Query'] = + ResolversParentTypes['Query'], +> = { + health?: Resolver; + notes?: Resolver< + Array, + ParentType, + ContextType, + RequireFields + >; +}; + +export type MutationResolvers< + ContextType = ApiContext, + ParentType extends ResolversParentTypes['Mutation'] = + ResolversParentTypes['Mutation'], +> = { + createNote?: Resolver< + ResolversTypes['Note'], + ParentType, + ContextType, + RequireFields + >; +}; + +export type NoteResolvers< + ContextType = ApiContext, + ParentType extends ResolversParentTypes['Note'] = + ResolversParentTypes['Note'], +> = { + id?: Resolver; + body?: Resolver; + createdAt?: Resolver; +}; + +export type Resolvers = { + Query?: QueryResolvers; + Mutation?: MutationResolvers; + Note?: NoteResolvers; +}; diff --git a/project2/api/src/schema.graphql b/project2/api/src/schema.graphql new file mode 100644 index 0000000..4dcbef9 --- /dev/null +++ b/project2/api/src/schema.graphql @@ -0,0 +1,15 @@ +""" +Development foundation only; catalogue operations belong in the Project 2 repository. +""" +type Query { + health: String! + notes(first: Int! = 20): [Note!]! +} +type Mutation { + createNote(body: String!): Note! +} +type Note { + id: ID! + body: String! + createdAt: String! +} diff --git a/project2/api/src/server.ts b/project2/api/src/server.ts new file mode 100644 index 0000000..5d07bcd --- /dev/null +++ b/project2/api/src/server.ts @@ -0,0 +1,31 @@ +import { serve } from '@hono/node-server'; +import { createApp } from './app.js'; +import { createRepository } from './db/repository.js'; +import { parseEnv } from './env.js'; +const env = parseEnv(process.env); +const repository = createRepository(env.DATABASE_URL); +const app = createApp( + repository, + env.CORS_ORIGINS, + env.NODE_ENV === 'production', +); +const server = serve( + { fetch: app.fetch, hostname: env.API_HOST, port: env.API_PORT }, + (info) => { + console.info(`Project 2 API: http://${env.API_HOST}:${info.port}/graphql`); + }, +); +let closing = false; +function shutdown() { + if (closing) return; + closing = true; + const deadline = setTimeout(() => process.exit(1), 10000).unref(); + server.close(() => { + void repository.close().then(() => { + clearTimeout(deadline); + process.exit(0); + }); + }); +} +process.on('SIGINT', shutdown); +process.on('SIGTERM', shutdown); diff --git a/project2/api/tsconfig.build.json b/project2/api/tsconfig.build.json new file mode 100644 index 0000000..4856e88 --- /dev/null +++ b/project2/api/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "dist", + "rootDir": "src", + "sourceMap": true + }, + "exclude": ["src/**/*.test.ts"] +} diff --git a/project2/api/tsconfig.json b/project2/api/tsconfig.json new file mode 100644 index 0000000..249dadd --- /dev/null +++ b/project2/api/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": ["node"], + "noEmit": true + }, + "include": ["src"] +} diff --git a/project2/contracts/generated.ts b/project2/contracts/generated.ts new file mode 100644 index 0000000..f796919 --- /dev/null +++ b/project2/contracts/generated.ts @@ -0,0 +1,186 @@ +// Generated by npm run types:api. Do not edit. +/** Internal type. DO NOT USE DIRECTLY. */ +type Exact = { [K in keyof T]: T[K] }; +/** Internal type. DO NOT USE DIRECTLY. */ +export type Incremental = + | T + | { + [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never; + }; +import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +export type Maybe = T | null; +export type InputMaybe = Maybe; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string }; + String: { input: string; output: string }; + Boolean: { input: boolean; output: boolean }; + Int: { input: number; output: number }; + Float: { input: number; output: number }; +}; + +/** Development foundation only; catalogue operations belong in the Project 2 repository. */ +export type Query = { + __typename?: 'Query'; + health: Scalars['String']['output']; + notes: Array; +}; + +/** Development foundation only; catalogue operations belong in the Project 2 repository. */ +export type QueryNotesArgs = { + first?: Scalars['Int']['input']; +}; + +export type Mutation = { + __typename?: 'Mutation'; + createNote: Note; +}; + +export type MutationCreateNoteArgs = { + body: Scalars['String']['input']; +}; + +export type Note = { + __typename?: 'Note'; + id: Scalars['ID']['output']; + body: Scalars['String']['output']; + createdAt: Scalars['String']['output']; +}; + +export type HealthQueryVariables = Exact<{ [key: string]: never }>; + +export type HealthQuery = { health: string }; + +export type NotesQueryVariables = Exact<{ + first?: number; +}>; + +export type NotesQuery = { + notes: Array<{ id: string; body: string; createdAt: string }>; +}; + +export type CreateNoteMutationVariables = Exact<{ + body: string; +}>; + +export type CreateNoteMutation = { + createNote: { id: string; body: string; createdAt: string }; +}; + +export const HealthDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: { kind: 'Name', value: 'Health' }, + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'health' } }, + ], + }, + }, + ], +} as unknown as DocumentNode; +export const NotesDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: { kind: 'Name', value: 'Notes' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { + kind: 'Variable', + name: { kind: 'Name', value: 'first' }, + }, + type: { + kind: 'NonNullType', + type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } }, + }, + defaultValue: { kind: 'IntValue', value: '20' }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'notes' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'first' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'first' }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'body' } }, + { kind: 'Field', name: { kind: 'Name', value: 'createdAt' } }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode; +export const CreateNoteDocument = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'mutation', + name: { kind: 'Name', value: 'CreateNote' }, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: { kind: 'Variable', name: { kind: 'Name', value: 'body' } }, + type: { + kind: 'NonNullType', + type: { + kind: 'NamedType', + name: { kind: 'Name', value: 'String' }, + }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: { kind: 'Name', value: 'createNote' }, + arguments: [ + { + kind: 'Argument', + name: { kind: 'Name', value: 'body' }, + value: { + kind: 'Variable', + name: { kind: 'Name', value: 'body' }, + }, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { kind: 'Field', name: { kind: 'Name', value: 'id' } }, + { kind: 'Field', name: { kind: 'Name', value: 'body' } }, + { kind: 'Field', name: { kind: 'Name', value: 'createdAt' } }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode; diff --git a/project2/contracts/operations.graphql b/project2/contracts/operations.graphql new file mode 100644 index 0000000..b225fd3 --- /dev/null +++ b/project2/contracts/operations.graphql @@ -0,0 +1,17 @@ +query Health { + health +} +query Notes($first: Int! = 20) { + notes(first: $first) { + id + body + createdAt + } +} +mutation CreateNote($body: String!) { + createNote(body: $body) { + id + body + createdAt + } +} diff --git a/roadmap.md b/roadmap.md index 2fa7482..c2e2a94 100644 --- a/roadmap.md +++ b/roadmap.md @@ -123,3 +123,4 @@ flowchart TD I14 --> I16[#16 Publisering] I15[#15 Dokumentasjon] --> I17[#17 Sluttkontroll] I16 --> I17 +``` diff --git a/scripts/copy-schema.mjs b/scripts/copy-schema.mjs new file mode 100644 index 0000000..c9d5b37 --- /dev/null +++ b/scripts/copy-schema.mjs @@ -0,0 +1,5 @@ +import { copyFile } from 'node:fs/promises'; +await copyFile( + new URL('../project2/api/src/schema.graphql', import.meta.url), + new URL('../project2/api/dist/schema.graphql', import.meta.url), +); diff --git a/scripts/database-smoke.ts b/scripts/database-smoke.ts new file mode 100644 index 0000000..ceb5039 --- /dev/null +++ b/scripts/database-smoke.ts @@ -0,0 +1,13 @@ +import assert from 'node:assert/strict'; +import { createRepository } from '../project2/api/src/db/repository.js'; +import { parseEnv } from '../project2/api/src/env.js'; +const env = parseEnv(process.env); +const db = createRepository(env.DATABASE_URL); +try { + await db.ping(); + const notes = await db.listNotes(1); + assert.equal(notes.length, 1, 'Run db:seed first'); + console.info('Native PostgreSQL connection and typed read passed.'); +} finally { + await db.close(); +} diff --git a/scripts/database.ts b/scripts/database.ts new file mode 100644 index 0000000..c91be06 --- /dev/null +++ b/scripts/database.ts @@ -0,0 +1,49 @@ +import { Pool } from 'pg'; +import { drizzle } from 'drizzle-orm/node-postgres'; +import { migrate } from 'drizzle-orm/node-postgres/migrator'; +import { notes } from '../project2/api/src/db/schema.js'; +import { parseEnv } from '../project2/api/src/env.js'; +const env = parseEnv(process.env); +const action = process.argv[2]; +if (!['migrate', 'seed', 'reset'].includes(action ?? '')) + throw new Error('Expected migrate, seed, or reset'); +if (action !== 'migrate') { + const url = new URL(env.DATABASE_URL); + if ( + env.NODE_ENV === 'production' || + !['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname) || + !['/t31_project2_dev', '/t31_project2_test'].includes(url.pathname) || + url.search + ) { + throw new Error( + 'Reset/seed only supports a dedicated loopback t31_project2_dev or t31_project2_test database, without URL parameters. Never use a tunnel.', + ); + } +} +const pool = new Pool({ + connectionString: env.DATABASE_URL, + connectionTimeoutMillis: 5000, +}); +const db = drizzle(pool); +try { + if (action === 'reset') + await pool.query( + 'DROP SCHEMA IF EXISTS project2 CASCADE; DROP SCHEMA IF EXISTS project2_migrations CASCADE;', + ); + if (action === 'migrate' || action === 'reset') + await migrate(db, { + migrationsFolder: './project2/api/migrations', + migrationsSchema: 'project2_migrations', + }); + if (action === 'seed' || action === 'reset') + await db + .insert(notes) + .values({ + id: '00000000-0000-4000-8000-000000000001', + body: 'Project 2 database foundation is ready.', + }) + .onConflictDoNothing(); + console.info(`Database ${action} completed.`); +} finally { + await pool.end(); +} diff --git a/scripts/env-init.mjs b/scripts/env-init.mjs new file mode 100644 index 0000000..50e5c00 --- /dev/null +++ b/scripts/env-init.mjs @@ -0,0 +1,11 @@ +import { copyFile } from 'node:fs/promises'; +import { constants } from 'node:fs'; +try { + await copyFile('.env.example', '.env', constants.COPYFILE_EXCL); + console.info( + 'Created .env; set your local PostgreSQL password before starting the optional API.', + ); +} catch (error) { + if (error.code !== 'EEXIST') throw error; + console.info('Existing .env preserved.'); +} diff --git a/scripts/generate-api.ts b/scripts/generate-api.ts new file mode 100644 index 0000000..5b32b69 --- /dev/null +++ b/scripts/generate-api.ts @@ -0,0 +1,50 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { codegen } from '@graphql-codegen/core'; +import * as typescript from '@graphql-codegen/typescript'; +import * as resolvers from '@graphql-codegen/typescript-resolvers'; +import * as operations from '@graphql-codegen/typescript-operations'; +import * as typedDocument from '@graphql-codegen/typed-document-node'; +import { parse } from 'graphql'; +import { format, resolveConfig } from 'prettier'; +const schema = parse(await readFile('project2/api/src/schema.graphql', 'utf8')); +const documents = [ + { + document: parse( + await readFile('project2/contracts/operations.graphql', 'utf8'), + ), + }, +]; +const targets = [ + { + filename: 'project2/api/src/generated.ts', + plugins: [{ typescript: {} }, { resolvers: {} }], + config: { + contextType: './db/repository.js#ApiContext', + useTypeImports: true, + }, + }, + { + filename: 'project2/contracts/generated.ts', + plugins: [{ typescript: {} }, { operations: {} }, { typedDocument: {} }], + config: { useTypeImports: true }, + }, +]; +for (const target of targets) { + const result = await codegen({ + ...target, + schema, + documents, + pluginMap: { typescript, resolvers, operations, typedDocument }, + }); + const output = await format( + '// Generated by npm run types:api. Do not edit.\n' + result, + { ...(await resolveConfig(target.filename)), filepath: target.filename }, + ); + if (process.argv.includes('--check')) { + if ((await readFile(target.filename, 'utf8')) !== output) + throw new Error(`${target.filename} is stale. Run npm run types:api.`); + } else { + await writeFile(target.filename, output); + } +} +console.info('GraphQL contracts are current.'); diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..71f8eb0 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2023", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/tsconfig.tools.json b/tsconfig.tools.json new file mode 100644 index 0000000..e17b6bc --- /dev/null +++ b/tsconfig.tools.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "types": ["node"], + "jsx": "react-jsx" + }, + "include": [ + "scripts/**/*.ts", + "e2e/**/*.ts", + "*.config.ts", + "web/vite.config.ts", + "project2/api/drizzle.config.ts", + "project2/contracts/**/*.ts" + ] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..ce31830 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +export default defineConfig({ + plugins: [react()], + test: { + projects: [ + { + extends: true, + test: { + name: 'web', + include: ['web/src/**/*.test.{ts,tsx}'], + environment: 'jsdom', + setupFiles: ['web/src/test/setup.ts'], + }, + }, + { + extends: true, + test: { + name: 'api', + include: ['project2/api/src/**/*.test.ts'], + environment: 'node', + }, + }, + ], + coverage: { + provider: 'v8', + include: ['web/src/**/*.{ts,tsx}', 'project2/api/src/**/*.ts'], + exclude: ['**/generated.ts', '**/test/**', '**/main.tsx', '**/server.ts'], + }, + }, +}); diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..cb4eee4 --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + Turvær Norge + + +
+ + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..67216f5 --- /dev/null +++ b/web/package.json @@ -0,0 +1,29 @@ +{ + "name": "@t31/web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "tsc -p tsconfig.app.json && vite build", + "preview": "vite preview --host 127.0.0.1" + }, + "dependencies": { + "@tanstack/react-query": "5.102.8", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "zod": "4.6.2" + }, + "devDependencies": { + "@babel/core": "7.29.7", + "@rolldown/plugin-babel": "0.2.4", + "@types/babel__core": "7.20.5", + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "@vitejs/plugin-react": "^6.1.1", + "babel-plugin-react-compiler": "1.0.0", + "typescript": "~6.0.2", + "vite": "^8.3.0" + } +} diff --git a/web/public/.gitkeep b/web/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/web/src/WeatherExample.test.tsx b/web/src/WeatherExample.test.tsx new file mode 100644 index 0000000..bc6021b --- /dev/null +++ b/web/src/WeatherExample.test.tsx @@ -0,0 +1,67 @@ +import { expect, it } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { http, HttpResponse } from 'msw'; +import { WeatherExample } from './WeatherExample'; +import { WEATHER_URL, fetchWeather } from './features/weather/weather'; +import { exampleCity } from './features/weather/cities'; +import { server } from './test/server'; +import { weatherResponse } from './test/fixtures'; +function renderExample() { + return render( + + + , + ); +} +it('renders validated weather and a stable snapshot', async () => { + const { asFragment } = renderExample(); + await screen.findByText('14 °C'); + expect( + screen.getByRole('heading', { name: 'Været i Trondheim' }), + ).toBeVisible(); + expect(screen.getByText('0.1 mm')).toBeVisible(); + expect(screen.getByText('3 m/s')).toBeVisible(); + expect(asFragment()).toMatchSnapshot(); +}); +it('shows errors and retries on user interaction', async () => { + server.use( + http.get(WEATHER_URL, () => new HttpResponse(null, { status: 503 })), + ); + renderExample(); + await screen.findByRole('alert'); + server.resetHandlers(); + await userEvent.click(screen.getByRole('button', { name: 'Prøv igjen' })); + await screen.findByText('14 °C'); +}); +it('rejects malformed provider data and unexpected units', async () => { + server.use( + http.get(WEATHER_URL, () => + HttpResponse.json({ + ...weatherResponse, + current_units: { + ...weatherResponse.current_units, + wind_speed_10m: 'km/h', + }, + }), + ), + ); + await expect(fetchWeather(exampleCity)).rejects.toThrow(); +}); +it('includes coordinates, explicit units, and cancellation in requests', async () => { + server.use( + http.get(WEATHER_URL, ({ request }) => { + const url = new URL(request.url); + expect(url.searchParams.get('latitude')).toBe( + String(exampleCity.latitude), + ); + expect(url.searchParams.get('wind_speed_unit')).toBe('ms'); + return HttpResponse.json(weatherResponse); + }), + ); + expect((await fetchWeather(exampleCity)).city.id).toBe('trondheim'); + const controller = new AbortController(); + controller.abort(); + await expect(fetchWeather(exampleCity, controller.signal)).rejects.toThrow(); +}); diff --git a/web/src/WeatherExample.tsx b/web/src/WeatherExample.tsx new file mode 100644 index 0000000..3045ab4 --- /dev/null +++ b/web/src/WeatherExample.tsx @@ -0,0 +1,63 @@ +import { useQuery } from '@tanstack/react-query'; +import { exampleCity } from './features/weather/cities'; +import { weatherQuery } from './features/weather/weather'; + +/** One working vertical example; build the roadmap features around these boundaries. */ +export function WeatherExample() { + const weather = useQuery(weatherQuery(exampleCity)); + return ( +
+
+

IT2810 · Gruppe 31

+

Turvær Norge

+

Finn været for din neste tur.

+
+
+

Været i {exampleCity.name}

+ {weather.isPending &&

Henter værdata…

} + {weather.isError && ( +
+

Kunne ikke hente værdata. Prøv igjen om litt.

+ +
+ )} + {weather.data && ( + <> +
+
+
Temperatur
+
{weather.data.current.temperature_2m} °C
+
+
+
Nedbør siste {weather.data.current.interval / 60} min
+
{weather.data.current.precipitation} mm
+
+
+
Vind
+
{weather.data.current.wind_speed_10m} m/s
+
+
+

+ Oppdatert{' '} + {' '} + (norsk tid). +

+ + )} +
+ +
+ ); +} diff --git a/web/src/__snapshots__/WeatherExample.test.tsx.snap b/web/src/__snapshots__/WeatherExample.test.tsx.snap new file mode 100644 index 0000000..57f485b --- /dev/null +++ b/web/src/__snapshots__/WeatherExample.test.tsx.snap @@ -0,0 +1,78 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`renders validated weather and a stable snapshot 1`] = ` + +
+
+

+ IT2810 · Gruppe 31 +

+

+ Turvær Norge +

+

+ Finn været for din neste tur. +

+
+
+

+ Været i Trondheim +

+
+
+
+ Temperatur +
+
+ 14 °C +
+
+
+
+ Nedbør siste 15 min +
+
+ 0.1 mm +
+
+
+
+ Vind +
+
+ 3 m/s +
+
+
+

+ Oppdatert + + (norsk tid). +

+
+ +
+
+`; diff --git a/web/src/features/weather/cities.ts b/web/src/features/weather/cities.ts new file mode 100644 index 0000000..04dc116 --- /dev/null +++ b/web/src/features/weather/cities.ts @@ -0,0 +1,18 @@ +export const cities = [ + { id: 'oslo', name: 'Oslo', latitude: 59.9139, longitude: 10.7522 }, + { id: 'bergen', name: 'Bergen', latitude: 60.393, longitude: 5.3242 }, + { id: 'trondheim', name: 'Trondheim', latitude: 63.4305, longitude: 10.3951 }, + { id: 'stavanger', name: 'Stavanger', latitude: 58.97, longitude: 5.7331 }, + { id: 'tromso', name: 'Tromsø', latitude: 69.6492, longitude: 18.9553 }, + { id: 'bodo', name: 'Bodø', latitude: 67.28, longitude: 14.405 }, + { + id: 'kristiansand', + name: 'Kristiansand', + latitude: 58.1467, + longitude: 7.9956, + }, + { id: 'alesund', name: 'Ålesund', latitude: 62.4722, longitude: 6.1495 }, +] as const; +export type City = (typeof cities)[number]; +export type CityId = City['id']; +export const exampleCity = cities[2]; diff --git a/web/src/features/weather/weather.ts b/web/src/features/weather/weather.ts new file mode 100644 index 0000000..ef440b6 --- /dev/null +++ b/web/src/features/weather/weather.ts @@ -0,0 +1,58 @@ +import { queryOptions } from '@tanstack/react-query'; +import { z } from 'zod'; +import type { City } from './cities'; + +export const WEATHER_URL = 'https://api.open-meteo.com/v1/forecast'; +export const weatherResponseSchema = z.object({ + timezone: z.literal('Europe/Oslo'), + current_units: z.object({ + temperature_2m: z.literal('°C'), + precipitation: z.literal('mm'), + wind_speed_10m: z.literal('m/s'), + }), + current: z.object({ + time: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/), + interval: z.number().int().positive(), + temperature_2m: z.number(), + precipitation: z.number().nonnegative(), + wind_speed_10m: z.number().nonnegative(), + weather_code: z.number().int().nonnegative(), + }), +}); +export type WeatherResponse = z.infer; + +export async function fetchWeather(city: City, signal?: AbortSignal) { + const url = new URL(WEATHER_URL); + url.search = new URLSearchParams({ + latitude: String(city.latitude), + longitude: String(city.longitude), + current: 'temperature_2m,precipitation,weather_code,wind_speed_10m', + wind_speed_unit: 'ms', + timezone: 'Europe/Oslo', + }).toString(); + const timeout = AbortSignal.timeout(15000); + const response = await fetch(url, { + signal: signal ? AbortSignal.any([signal, timeout]) : timeout, + cache: 'no-store', + credentials: 'omit', + }); + if (!response.ok) + throw new Error(`Weather API returned HTTP ${response.status}`); + const body: unknown = await response.json(); + const parsed = weatherResponseSchema.parse(body); + return { city, current: parsed.current }; +} +export type CityWeather = Awaited>; + +export function weatherQuery(city: City) { + return queryOptions({ + queryKey: ['weather', city.id, city.latitude, city.longitude], + queryFn: ({ signal }) => fetchWeather(city, signal), + // Only retain the active response in memory; reloads fetch live data. + staleTime: Infinity, + gcTime: 0, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + retry: false, + }); +} diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..573102f --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,15 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { WeatherExample } from './WeatherExample'; +import './styles.css'; +const root = document.getElementById('root'); +if (!root) throw new Error('Missing root element'); +const queryClient = new QueryClient(); +createRoot(root).render( + + + + + , +); diff --git a/web/src/shared/storage.ts b/web/src/shared/storage.ts new file mode 100644 index 0000000..615f0e6 --- /dev/null +++ b/web/src/shared/storage.ts @@ -0,0 +1,31 @@ +import { useState } from 'react'; +import type { z } from 'zod'; +export function useStoredState( + area: 'localStorage' | 'sessionStorage', + key: string, + schema: z.ZodType, + initial: T, +) { + const [value, setValue] = useState(() => { + try { + const raw = window[area].getItem(key); + if (raw === null) return initial; + const decoded: unknown = JSON.parse(raw); + const parsed = schema.safeParse(decoded); + return parsed.success ? parsed.data : initial; + } catch { + return initial; + } + }); + const [storageFailed, setStorageFailed] = useState(false); + function update(next: T) { + setValue(next); + try { + window[area].setItem(key, JSON.stringify(next)); + setStorageFailed(false); + } catch { + setStorageFailed(true); + } + } + return [value, update, storageFailed] as const; +} diff --git a/web/src/styles.css b/web/src/styles.css new file mode 100644 index 0000000..8943805 --- /dev/null +++ b/web/src/styles.css @@ -0,0 +1,64 @@ +:root { + font-family: system-ui, sans-serif; + line-height: 1.6; + color: #173d35; + background: #f2f5f0; +} +* { + box-sizing: border-box; +} +body { + margin: 0; +} +main { + width: min(680px, 100% - 2rem); + margin: 3rem auto; +} +h1 { + font-size: clamp(2rem, 6vw, 3rem); + line-height: 1.1; +} +header { + margin-bottom: 2rem; +} +section { + border: 1px solid #bacac1; + background: #fff; + padding: clamp(1rem, 4vw, 2rem); + border-radius: 0.5rem; +} +dl { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; +} +dt { + font-size: 0.85rem; +} +dd { + margin: 0.25rem 0; + font-size: 1.5rem; + font-weight: 600; +} +button { + font: inherit; + min-height: 44px; + padding: 0.5rem 1rem; + background: #173d35; + color: white; + border: 0; + border-radius: 0.25rem; + cursor: pointer; +} +a { + color: inherit; + text-underline-offset: 0.2em; +} +:focus-visible { + outline: 3px solid #9a491c; + outline-offset: 4px; +} +footer { + margin-top: 2rem; + font-size: 0.85rem; +} diff --git a/web/src/test/fixtures.ts b/web/src/test/fixtures.ts new file mode 100644 index 0000000..3a201cb --- /dev/null +++ b/web/src/test/fixtures.ts @@ -0,0 +1,17 @@ +import type { WeatherResponse } from '../features/weather/weather'; +export const weatherResponse: WeatherResponse = { + timezone: 'Europe/Oslo', + current_units: { + temperature_2m: '°C', + precipitation: 'mm', + wind_speed_10m: 'm/s', + }, + current: { + time: '2026-09-11T12:00', + interval: 900, + temperature_2m: 14, + precipitation: 0.1, + wind_speed_10m: 3, + weather_code: 3, + }, +}; diff --git a/web/src/test/server.ts b/web/src/test/server.ts new file mode 100644 index 0000000..49b15ad --- /dev/null +++ b/web/src/test/server.ts @@ -0,0 +1,7 @@ +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import { WEATHER_URL } from '../features/weather/weather'; +import { weatherResponse } from './fixtures'; +export const server = setupServer( + http.get(WEATHER_URL, () => HttpResponse.json(weatherResponse)), +); diff --git a/web/src/test/setup.ts b/web/src/test/setup.ts new file mode 100644 index 0000000..a955850 --- /dev/null +++ b/web/src/test/setup.ts @@ -0,0 +1,14 @@ +import '@testing-library/jest-dom/vitest'; +import { afterAll, afterEach, beforeAll, beforeEach } from 'vitest'; +import { cleanup } from '@testing-library/react'; +import { server } from './server'; +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); +beforeEach(() => { + localStorage.clear(); + sessionStorage.clear(); +}); +afterEach(() => { + cleanup(); + server.resetHandlers(); +}); +afterAll(() => server.close()); diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json new file mode 100644 index 0000000..b58a033 --- /dev/null +++ b/web/tsconfig.app.json @@ -0,0 +1,27 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..069e2fb --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1 @@ +{ "files": [], "references": [{ "path": "./tsconfig.app.json" }] } diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..47e7d29 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite'; +import react, { reactCompilerPreset } from '@vitejs/plugin-react'; +import babel from '@rolldown/plugin-babel'; +export default defineConfig({ + base: '/project1/', + plugins: [react(), babel({ presets: [reactCompilerPreset()] })], + server: { port: 5173, strictPort: true }, + preview: { port: 4173, strictPort: true }, +}); From 29f779c373b0b30e0a4eacd389564cf7597eec37 Mon Sep 17 00:00:00 2001 From: Oliver Dragland Date: Fri, 11 Sep 2026 11:34:22 +0200 Subject: [PATCH 04/21] readme.md update --- README.md | 289 +++++++++++++------------------------------- docs/ai-use.md | 9 -- docs/development.md | 104 ++++++++++++++++ 3 files changed, 190 insertions(+), 212 deletions(-) delete mode 100644 docs/ai-use.md create mode 100644 docs/development.md diff --git a/README.md b/README.md index 95b9ae1..ebeaa0a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ -# Turvær Norge — T31 architecture skeleton +# Turvær Norge -## Development setup +Prosjekt 1 i **IT2810 Webutvikling** ved NTNU. -Use Node **24.6+ within the 24 series** and npm **11**. The checkout pins Node 24.21.0; Node 22 does not satisfy the assignment. +Turvær Norge er en responsiv React-applikasjon som henter oppdaterte værdata for norske byer. Brukeren kan velge aktivitet, sammenligne byer og finne ut hvor været passer best til for eksempel gåtur, sykling eller utendørsaktiviteter. + +## Kom i gang + +Bruk Node **24.6+** og npm **11**. Med nvm: ```bash git clone --branch dev git@git.ntnu.no:IT2810-H26/T31-Project-1.git @@ -14,229 +18,108 @@ npm ci npm run dev ``` -Open . If nvm is not installed, follow the [Node installation guide](https://nodejs.org/en/download) or use fnm with `.node-version`. - -Bun is an optional script launcher: `bun dev`, `bun types:api`, `bun db:reset`, and `bun run check` work with Node/npm available on PATH. The course explicitly specifies Node and npm; retain `package-lock.json` and use **npm ci** to install. Do not maintain competing Bun/npm lockfiles. `bun test` invokes Bun's own runner, so use **bun run test** for required Vitest tests. If Bun is installed at `~/.bun/bin/bun`, add `export PATH="$HOME/.bun/bin:$PATH"` to your shell configuration if needed. - -### Everyday development tools - -```bash -npm run dev # Single weather example: http://127.0.0.1:5173/project1/ -npm run test:watch # Vitest while editing -npm run check # Formatting, lint, types, tests and builds -npm run format # Apply Prettier -npm run types:api # Regenerate the optional GraphQL contracts -npm run build # Static client output: web/dist/ -npm run preview # Preview that production build -``` - -Optional Project 2 work, after the native PostgreSQL setup below: - -```bash -npm run env:init -npm run db:migrate -npm run db:seed -npm run dev:api # Node GraphQL API: http://127.0.0.1:3001/graphql -npm run dev:all # Example frontend and API together -npm run db:reset # DESTRUCTIVE: dedicated local development schemas only -``` - -Bun equivalents include `bun dev`, `bun types:api`, `bun db:reset` and `bun run check`. Keep Node 24/npm 11 available even when launching commands with Bun. The full command reference and Docker alternative are below. - -## Scope - -Architecture skeleton for **Turvær Norge**, the team's weather/activity project described on the remote `dev` branch. The eventual app will help users choose an outdoor activity and compare weather across at least eight Norwegian cities. See the preserved [team roadmap](roadmap.md). - -**Scope:** tooling, typed boundaries and **one example screen**, [`WeatherExample.tsx`](web/src/WeatherExample.tsx), fetching current weather for Trondheim. This is not the completed Project 1 app. City browsing, activity recommendations, filters, sorting, favorites, full component composition and final design belong to future issues. Eight typed city definitions are provided as a starting point for team agreement. - -The separate `project2/` directory prepares native PostgreSQL and a Node/Hono/GraphQL API as requested. Project 1 runs without that backend, database, Docker, API keys or environment files. See [requirement traceability](docs/requirements.md), [testing](docs/testing.md) and [AI use](docs/ai-use.md). - -## Why this stack differs from the proposal - -| Technology | Decision | -| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| React, TypeScript, Vite | Required. Scaffolded with `npm create vite@latest -- --template react-ts`; React Compiler added per the environment notes. | -| TanStack Query | Required for Project 1 REST requests. | -| Plain CSS, original components | Required for Project 1. No Tailwind, shadcn, or component library. | -| Hono | Optional Project 2 HTTP host; GraphQL Yoga supplies the required GraphQL API. Uses Node in development and deployment. | -| PostgreSQL + Drizzle | Project 2 requires a database installed directly on the group VM. PostgreSQL meets this; Drizzle gives typed queries and reviewable SQL migrations. | -| Supabase | Not required. Hosted Supabase conflicts with the VM database requirement; its typical Docker deployment conflicts with the direct-install rule. Use native PostgreSQL. | -| Redis | Omitted. Project 1 forbids storing API resources. Project 2 does not require Redis; introduce caching only for a measured need later. | -| Docker | Optional frontend development convenience only. Both delivered clients must be served by Apache; Project 2 backend/database must run directly on the VM. | -| Bun | Optional script runner; Node 24/npm 11 remain the documented reproducible baseline. | - -The bundled Project 2 specification says **under work**. Recheck Canvas before expanding it. Do not assume later assignments permit Project 1's choices or vice versa. - -## Architecture and type safety - -```text -web/src/ - features/weather/ Typed city definitions, validated REST boundary, Query options - shared/storage.ts Validated, versioned browser preferences - test/ Offline fixtures and MSW request mocking - WeatherExample.tsx The single example screen: live Trondheim weather -project2/ - api/src/schema.graphql Single GraphQL contract source - api/src/generated.ts Generated resolver types - api/src/db/schema.ts Drizzle schema; inferred select/insert types - api/src/db/repository.ts Typed persistence boundary - api/migrations/ Generated, committed SQL and migration metadata - contracts/ Operations and generated typed documents for a future client -scripts/ Contract generation, safe local DB commands, environment setup -``` - -The Bedrockchain reference informed strict TypeScript, runtime boundary validation, generated contracts with drift checking, inferred database types, root commands, and migration/reset/seed workflows. Its infrastructure and UI libraries were not copied because course requirements differ. - -- External REST JSON is `unknown` until Zod validates it. UI types are inferred from that schema. -- GraphQL SDL generates resolver types and typed client documents; `types:check` detects stale output without rewriting it. -- Database row/insert types derive from Drizzle (`NoteRow`/`NewNote`); `types:db` checks these rather than generating a duplicate Supabase type file. -- Strict mode, unchecked-index checking, exact optional properties, lint and formatting apply throughout. -- The example imports nothing from `project2/`. Its build contains no database credentials or backend code. - -## Project 1 data and state boundaries - -The example uses the [Open-Meteo forecast API](https://open-meteo.com/en/docs), a keyless REST provider proposed for issue #4. The typed city list proposes Oslo, Bergen, Trondheim, Stavanger, Tromsø, Bodø, Kristiansand and Ålesund. Confirm final cities/activities/API with the team; the skeleton does not close the planning issues. - -`features/weather/weather.ts` requests current temperature, precipitation, wind speed and WMO weather code. Wind units are explicitly **m/s**, timestamps use **Europe/Oslo**, and precipitation is labelled with the provider's interval (currently 15 minutes), not presented as a daily total. The Zod schema rejects malformed values or unexpected units before data reaches the UI. The example displays three metrics; mapping WMO codes to human-readable weather types is future domain work. - -TanStack Query holds the active response in memory, with no persistent query cache. `gcTime: 0` discards unobserved results, `cache: no-store` avoids browser HTTP storage, reload fetches fresh data, and focus/reconnect refetch and automatic retries are disabled. User retry is explicit. Development StrictMode can cancel/restart an initial request; production smoke tests check one request. Decide an explicit refresh policy when implementing long-lived weather views. - -`shared/storage.ts` is a reusable validated storage hook for future preference and favorite-ID state. The example **does not implement favorites or persist weather**. Use namespaced/versioned keys, session storage for preferences and local storage for favorite city IDs; never store the weather payload. When building the full client, show one city resource at a time with previous/next and direct selection, as required by Project 1. The team's roadmap mentions multiple cards; adapt that presentation to the course requirement while keeping all eight cities selectable. - -CORS must be permitted by the external API; frontend headers cannot grant it. There is no local proxy hiding a production restriction. Project 2 has its own explicit origin allowlist and credentials disabled; CORS is not authentication. Open-Meteo attribution and the CC BY 4.0 link are present in the example. Review the provider's terms, noncommercial usage allowance and limits before delivery. - -## Commands - -All commands below are `npm run ` or `bun run `; non-reserved names also support `bun `. - -| Command | Purpose | -| ----------------------------------------- | -------------------------------------------------------------------------------------- | -| `dev`, `dev:web` | Project 1 Vite server | -| `dev:api` | Optional Project 2 Node API with watch | -| `dev:all` | Both servers; requires local PostgreSQL configuration for API operations | -| `build`, `preview` | Build/preview Project 1 at `/project1/` | -| `build:api`, `start:api` | Compile/run the Project 2 Node API | -| `setup` | npm ci, preserve/create .env, generate API types | -| `env:init` | Copy .env.example only if .env is missing | -| `types:api`, `types:check` | Generate/check GraphQL resolver and operation types | -| `types:db`, `typecheck` | Inferred DB types / all strict TypeScript checks | -| `db:generate` | Generate a migration after changing Drizzle schema | -| `db:migrate` | Apply committed migrations to DATABASE_URL | -| `db:seed` | Idempotent development note seed | -| `db:reset` | Drop only project2/project2_migrations schemas, migrate, seed a guarded local database | -| `db:check`, `test:db` | Check migration metadata / verify a seeded native PostgreSQL read | -| `lint`, `lint:fix` | ESLint and optional fixes | -| `format`, `format:check` | Prettier write/check | -| `test`, `test:watch`, `test:coverage` | Vitest, always offline with mocks | -| `test:e2e:install`, `test:e2e` | Install browser engines / run Playwright and axe checks | -| `check` | Formatting, lint, contract drift, types, migration metadata, unit tests, both builds | -| `docker:up`, `docker:down`, `docker:logs` | Optional frontend-only development container | - -`check` does not require PostgreSQL or downloaded browsers. Run `test:db` and `test:e2e` separately when their prerequisites are available. - -## Native PostgreSQL setup for Project 2 - -Do this only when working on the optional backend. On Debian/Ubuntu install PostgreSQL directly: - -```bash -sudo apt install postgresql postgresql-client -sudo systemctl start postgresql -sudo -u postgres createuser --pwprompt t31_dev -sudo -u postgres createdb --owner=t31_dev t31_project2_dev -npm run env:init -``` - -Edit `.env` with your chosen password (URL-encode special characters). Do not commit it. Then: - -```bash -npm run db:migrate -npm run db:seed -npm run test:db -npm run dev:api -``` +Åpne . Uten nvm: installer versjonen i `.node-version` med valgfri Node-versjonsbehandler. Registrer SSH-nøkkelen din på [git.ntnu.no](https://git.ntnu.no/settings/keys) før kloning. -Open for development GraphiQL. `/health` reports the process; `/ready` checks the database. Try: - -```graphql -query { - notes(first: 10) { - id - body - createdAt - } -} -mutation { - createNote(body: "Our first persisted note") { - id - body - } -} -``` +Repoet inneholder foreløpig grunnoppsett og ett [væreksempel for Trondheim](web/src/WeatherExample.tsx). Resten utvikles gjennom [roadmap](roadmap.md). Prosjekt 1 trenger ingen database eller `.env` for å starte. -This notes example demonstrates typed persistence and validation; it is not the finished Project 2 catalogue. It has no user authentication or authorization and binds to loopback by default. Before making it publicly writable, design identity/ownership, abuse controls, GraphQL query-cost limits, pagination and domain-specific behavior. +### Alle npm-kommandoer -`db:reset` is destructive for its two project schemas. It refuses production mode, non-loopback hosts, URL parameters and database names other than `t31_project2_dev`/`t31_project2_test`. Do not point a loopback tunnel at a remote database. It does not delete other schemas or reset system PostgreSQL. `db:migrate` is intentionally usable on the eventual VM deployment; inspect its target first. +| Kommando | Bruk | +| -------------------------- | ----------------------------------------------------- | +| `npm run setup` | Installer avhengigheter, opprett .env, generer typer | +| `npm run env:init` | Opprett .env uten å overskrive eksisterende | +| `npm run dev` | Start frontend | +| `npm run dev:web` | Alias for dev | +| `npm run dev:api` | Start Prosjekt 2-API | +| `npm run dev:all` | Start frontend og API | +| `npm run build` | Bygg frontend til web/dist/ | +| `npm run build:api` | Bygg Node-API | +| `npm run preview` | Forhåndsvis frontendbygget | +| `npm run start:api` | Kjør bygget API | +| `npm run typecheck` | Sjekk all TypeScript | +| `npm run types:api` | Generer GraphQL-typer | +| `npm run types:check` | Sjekk at genererte typer er oppdatert | +| `npm run types:db` | Sjekk databasetyper | +| `npm run db:generate` | Generer SQL-migrasjon | +| `npm run db:migrate` | Kjør migrasjoner | +| `npm run db:seed` | Legg inn lokale eksempeldata | +| `npm run db:reset` | Slett lokale prosjektskjemaer, migrer og seed | +| `npm run db:check` | Sjekk migrasjonsmetadata | +| `npm run lint` | Kjør ESLint | +| `npm run lint:fix` | Rett lint-feil automatisk | +| `npm run format` | Formater med Prettier | +| `npm run format:check` | Sjekk formatering | +| `npm run test` | Kjør Vitest | +| `npm run test:watch` | Vitest i watch-modus | +| `npm run test:coverage` | Mål testdekning | +| `npm run test:e2e` | Kjør Playwright | +| `npm run test:e2e:install` | Installer testnettlesere | +| `npm run test:db` | Sjekk databasetilkobling og lesing | +| `npm run check` | Formatering, lint, typer, migrasjoner, tester og bygg | +| `npm run docker:up` | Bygg/start frontend-container | +| `npm run docker:down` | Stopp frontend-container | +| `npm run docker:logs` | Vis containerlogger | -For schema changes: edit `schema.ts`, run `db:generate`, inspect SQL, migrate/reset a dedicated local DB, run `types:api` when SDL changes, and run `check` plus `test:db`. Commit migrations and generated contracts. `db:check` validates migration metadata, not live database drift. +Du kan også bruke `bun dev`, `bun types:api` og `bun run check`. Installer avhengigheter med `npm ci`; behold én lockfil. Bruk `bun run test` for Vitest, siden `bun test` starter Buns egen testkjører. -## Authentication and publishing to NTNU +## Konsept -Remote: `git@git.ntnu.no:IT2810-H26/T31-Project-1.git`. +Applikasjonen skal: -NTNU GitHub Enterprise and github.com are separate hosts/accounts. Your github.com authentication does not automatically log you into NTNU, but the same SSH public key can be registered with both. +- hente ferske værdata fra et åpent REST API +- vise værinformasjon for minst 8 norske byer +- la brukeren velge aktivitet +- filtrere og sortere byer etter værforhold +- vise temperatur, nedbør, vind og værtype +- vise tydelig lasting og feilmelding ved API-feil +- fungere på både mobil og desktop -1. Sign in to with your school account and verify access to the T31 repo. -2. On this machine, run `cat ~/.ssh/id_ed25519.pub`. Register that **public** key at . Never share `id_ed25519` without `.pub`. -3. Run `ssh -T git@git.ntnu.no`. On first connection, verify the server fingerprint with NTNU before accepting. Successful GitHub SSH authentication may exit with status 1 because it does not offer shell access. -4. Run `git ls-remote origin` from this repository to verify repository access. +Værdata hentes direkte når applikasjonen brukes og mellomlagres ikke. -To keep distinct keys, use `ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_ntnu -C "your-ntnu-email"`, register its `.pub` file, then add this host-specific entry to `~/.ssh/config`: +## Teknologi -```sshconfig -Host git.ntnu.no - HostName git.ntnu.no - User git - IdentityFile ~/.ssh/id_ed25519_ntnu - IdentitiesOnly yes -``` +React, TypeScript, Vite, TanStack Query, `fetch`, plain CSS, ESLint, Prettier og Vitest. Eksemplet bruker [Open-Meteo](https://open-meteo.com/en/docs). -This does not change github.com settings. If the key is passphrase-protected, add it to your agent with `ssh-add ~/.ssh/id_ed25519_ntnu`. +Alle UI-komponenter utvikles av gruppen. Prosjektet bruker ikke Bootstrap, Tailwind, Material UI eller andre ferdige UI-komponentbiblioteker. -For issues/PRs through the CLI, authenticate separately with `gh auth login --hostname git.ntnu.no --git-protocol ssh --web`. An SSH key is enough for git push; it is not API/CLI authentication. If web/device login is unavailable on this Enterprise version, use its documented token flow in your own terminal. Do not paste tokens into chat. +`project2/` forbereder Node/Hono, GraphQL og native PostgreSQL. Databasekommandoene krever [lokalt PostgreSQL-oppsett](docs/development.md#native-postgresql-setup-for-project-2). `db:reset` sletter lokale prosjektskjemaer. Docker er kun for frontendutvikling; levering skjer via Apache. -SSH was verified on this machine as `oliverdr`, using the dedicated `~/.ssh/id_ed25519_ntnu` key. Host-specific configuration preserves github.com access. No NTNU GitHub CLI/API login was assumed. +## Brancher og commits -This initial `init` commit is prepared directly on `dev` at the user's request, preserving its existing concept, roadmap and CODEOWNERS history. Main remains protected. For subsequent work, create a feature branch and open a pull request following the team's integration process. This is **GitHub Enterprise**, so use Pull requests, not the GitLab Merge requests wording in the earlier README. +Bruk alltid én av typene `fix`, `comment` eller `feature`: -The repository's review rules remain: +- `fix`: retting av feil. +- `comment`: dokumentasjon eller kodekommentarer. +- `feature`: ny funksjonalitet. -1. A member of `IT2810-H26-T31` must approve; self-approval and staff approval do not count. -2. All review conversations must be resolved. -3. New commits dismiss existing approval; request another review after changes. -4. Never force-push shared branches or commit secrets. Select an issue before new feature work and link it in commits/PRs. +**Branch:** `/-
` — små bokstaver og bindestreker, uten mellomrom eller kolon. -Optionally configure your school commit email for this repo only: `git config user.email "your-ntnu-email"`. This affects commit attribution, not SSH authentication. +**Commit og PR-tittel:** `():
`. -## Apache deployment +| Branch | Commit / PR-tittel | +| ----------------------------------- | ----------------------------------------- | +| `fix/weather-handle-timeout` | `fix(weather): handle request timeout` | +| `comment/docs-setup-guide` | `comment(docs): clarify local setup` | +| `feature/weather-activity-selector` | `feature(weather): add activity selector` | -Project 1 must be served by Apache on the group VM at `/project1/`. `base: '/project1/'` is already configured. Campus network or NTNU VPN is required to reach the VM. Verify that the assigned hostname is `it2810-31.idi.ntnu.no` before using it. +Eksempel på arbeidsflyt: ```bash -npm ci -npm run check -npm run test:e2e -# Set these to the confirmed account/host: -# scp -r web/dist YOUR_NTNU_USER@it2810-31.idi.ntnu.no:~/t31-project1-dist +git switch dev +git pull --ff-only origin dev +git switch -c feature/weather-activity-selector +# Gjør endringene og kjør npm run check. +git add +git commit -m "feature(weather): add activity selector" +git push -u origin feature/weather-activity-selector ``` -On the VM, install Apache with `sudo apt install apache2`, then copy the **contents** of the built directory into `/var/www/html/project1/` (for example `sudo install -d /var/www/html/project1` followed by `sudo cp -a ~/t31-project1-dist/. /var/www/html/project1/`). Avoid nesting `dist` inside `project1`. Use a fresh staging directory per release; back up any existing deployment before replacing it. Verify `/project1/`, a direct reload, API access, weather loading, keyboard access and console output through Apache. This starter uses no client router, so no SPA rewrite rule is needed. - -Project 2's eventual client belongs at `/project2/`. Run its built API on Node, ideally as a systemd service with an environment file, and PostgreSQL directly on the VM. The preferred backend port is 3001. Apache can reverse-proxy `/project2-api/` to `http://127.0.0.1:3001/`; keep the API bound to loopback for that setup. A client then uses `/project2-api/graphql`. Service configuration, final domain, deployment permissions and credentials remain team tasks. - -## Team process and delivery +Velg et issue før du starter, og lenk det i PR-en, for eksempel `Closes #8`. Åpne en **Pull request** til `dev`; integrer deretter i beskyttet `main` gjennom review. -Plan changes in issues; link branches/commits/PRs; require teammate review and record verification. Each member should understand and improve generated code. Keep individual time/contribution logs out of Git and deliver them through the course's specified channel. +- En annen student i IT2810-H26-T31 må godkjenne. Egen godkjenning eller godkjenning fra fagstaben teller ikke. +- Alle review-kommentarer må være løst. Nye commits krever ny godkjenning. +- Ikke push direkte til `main`, force-push delte brancher eller commit `.env` og hemmeligheter. -The downloaded Canvas schedule lists Project 1 delivery **18 September**, peer review **25 September**, and final delivery **27 November 2026**. Verify the live schedule. The materials disagree on some overall assessment percentages; use current Canvas/staff guidance. Meeting the requirement list alone is described as a satisfactory baseline, not an A guarantee. Excellent usability, design, technical understanding, testing and collaboration must be demonstrated over the project. +## Dokumentasjon -NTNU Enterprise does not supply hosted Actions runners. Do not assume `ubuntu-latest` will run. `npm run check` is ready for a future self-hosted runner; the course says runner setup may require staff-granted repository admin access. No nonfunctional workflow is installed here. +[Roadmap](roadmap.md) · [Utviklerguide og planlagt dataflyt](docs/development.md) · [Krav og gjenstående arbeid](docs/requirements.md) · [Tester](docs/testing.md) diff --git a/docs/ai-use.md b/docs/ai-use.md deleted file mode 100644 index cbe5044..0000000 --- a/docs/ai-use.md +++ /dev/null @@ -1,9 +0,0 @@ -# AI assistance log - -## 2026-09-11 — architecture skeleton - -The user asked Codex to read local IT2810 materials and a sibling Bedrockchain reference, adapt the proposed stack, and prepare typed architecture/developer scripts. They requested Project 2 database groundwork, then clarified that the deliverable must stay a skeleton with a single example file. - -After SSH access was established, Codex read the remote dev README and roadmap. The skeleton now follows the team's Turvær Norge concept with one Trondheim weather example and typed definitions for eight proposed cities. The final scope leaves Project 1 feature implementation to team issues. Native PostgreSQL and Node GraphQL replace the originally suggested Supabase/Docker backend to match course constraints. - -AI assisted with analysis, configuration, implementation, docs and automated checks. The team must review/understand generated code, confirm API/cities/activities, complete the roadmap and document each member's actual work. Automated checks are not a substitute for human review, real-device testing or demonstrated course knowledge. diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..0d0d844 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,104 @@ +# Development reference + +The README covers everyday setup. This page contains optional infrastructure commands and deployment details. + +## Structure and type safety + +- `web/src/WeatherExample.tsx`: the single weather example. +- `web/src/features/weather/`: typed city definitions, Zod response validation and TanStack Query options. +- `web/src/shared/storage.ts`: storage helper for future preferences and favorite city IDs. +- `project2/api/`: independent Node/Hono/GraphQL API with native PostgreSQL and Drizzle migrations. +- `project2/contracts/`: generated resolver/client contracts; regenerate with `npm run types:api`. + +Project 1 calls Open-Meteo directly. Wind is requested in m/s, timestamps use Europe/Oslo and precipitation is labelled with the provider's interval. Only the active response is held in memory; weather payloads are never persisted. Future storage features should store preferences and city IDs only. + +Project 2 requires GraphQL and a database installed directly on the VM. Hosted Supabase and Docker backend/database deployment do not fit that requirement. Docker here is only an optional frontend development tool; Redis is not needed. + +## Native PostgreSQL setup for Project 2 + +Do this only when working on the optional backend. On Debian/Ubuntu install PostgreSQL directly: + +```bash +sudo apt install postgresql postgresql-client +sudo systemctl start postgresql +sudo -u postgres createuser --pwprompt t31_dev +sudo -u postgres createdb --owner=t31_dev t31_project2_dev +npm run env:init +``` + +Edit `.env` with your chosen password (URL-encode special characters). Do not commit it. Then: + +```bash +npm run db:migrate +npm run db:seed +npm run test:db +npm run dev:api +``` + +Open for development GraphiQL. `/health` reports the process; `/ready` checks the database. Try: + +```graphql +query { + notes(first: 10) { + id + body + createdAt + } +} +mutation { + createNote(body: "Our first persisted note") { + id + body + } +} +``` + +This notes example demonstrates typed persistence and validation; it is not the finished Project 2 catalogue. It has no user authentication or authorization and binds to loopback by default. Before making it publicly writable, design identity/ownership, abuse controls, GraphQL query-cost limits, pagination and domain-specific behavior. + +`db:reset` is destructive for its two project schemas. It refuses production mode, non-loopback hosts, URL parameters and database names other than `t31_project2_dev`/`t31_project2_test`. Do not point a loopback tunnel at a remote database. It does not delete other schemas or reset system PostgreSQL. `db:migrate` is intentionally usable on the eventual VM deployment; inspect its target first. + +For schema changes: edit `schema.ts`, run `db:generate`, inspect SQL, migrate/reset a dedicated local DB, run `types:api` when SDL changes, and run `check` plus `test:db`. Commit migrations and generated contracts. `db:check` validates migration metadata, not live database drift. + +## Apache deployment + +Project 1 must be served by Apache on the group VM at `/project1/`. `base: '/project1/'` is already configured. Campus network or NTNU VPN is required to reach the VM. Verify that the assigned hostname is `it2810-31.idi.ntnu.no` before using it. + +```bash +npm ci +npm run check +npm run test:e2e +# Set these to the confirmed account/host: +# scp -r web/dist YOUR_NTNU_USER@it2810-31.idi.ntnu.no:~/t31-project1-dist +``` + +On the VM, install Apache with `sudo apt install apache2`, then copy the **contents** of the built directory into `/var/www/html/project1/` (for example `sudo install -d /var/www/html/project1` followed by `sudo cp -a ~/t31-project1-dist/. /var/www/html/project1/`). Avoid nesting `dist` inside `project1`. Use a fresh staging directory per release; back up any existing deployment before replacing it. Verify `/project1/`, a direct reload, API access, weather loading, keyboard access and console output through Apache. This starter uses no client router, so no SPA rewrite rule is needed. + +Project 2's eventual client belongs at `/project2/`. Run its built API on Node, ideally as a systemd service with an environment file, and PostgreSQL directly on the VM. The preferred backend port is 3001. Apache can reverse-proxy `/project2-api/` to `http://127.0.0.1:3001/`; keep the API bound to loopback for that setup. A client then uses `/project2-api/graphql`. Service configuration, final domain, deployment permissions and credentials remain team tasks. + +## Planned Project 1 structure + +The following diagrams are retained from the team's project plan; these components are not implemented by the skeleton. + +## Dataflyt + +```mermaid +flowchart LR + A[Brukeren velger aktivitet] --> B[React oppdaterer state] + B --> C[Værtjenesten henter data] + C --> D[Vær-API] + D --> E[Filtrering og sortering] + E --> F[Værkort og detaljvisning] +``` + +## Komponentflyt + +```mermaid +flowchart TD + App --> ActivitySelector + App --> WeatherFilters + App --> WeatherList + WeatherList --> WeatherCard + App --> WeatherDetails + App --> LoadingMessage + App --> ErrorMessage +``` From bb9ad62302187a646769179610c0515f4299b7d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20=C3=98rjavik?= Date: Fri, 11 Sep 2026 17:37:41 +0200 Subject: [PATCH 05/21] feature(weather): add App layout and activity selector Refs #6, #8 --- web/src/App.tsx | 140 ++++++++++++++++++++++++++++++++ web/src/WeatherExample.test.tsx | 21 +++++ web/src/main.tsx | 4 +- 3 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 web/src/App.tsx diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..33dcae6 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,140 @@ +import { useEffect, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { cities, exampleCity, type City } from './features/weather/cities'; +import { weatherQuery, type CityWeather } from './features/weather/weather'; + +const activities = ['Gåtur', 'Løpetur', 'Skitur'] as const; +type Activity = (typeof activities)[number]; + +type WeatherContentProps = { + city: City; + activity: Activity; + weather: CityWeather | null; + isLoading: boolean; + error: Error | null; + onRetry: () => void; +}; + +function WeatherContent({ city, activity, weather, isLoading, error, onRetry }: WeatherContentProps) { + return ( +
+

Været i {city.name}

+

Valgt aktivitet: {activity}

+ {isLoading &&

Henter værdata…

} + {error && ( +
+

Kunne ikke hente værdata. Prøv igjen om litt.

+ +
+ )} + {weather && ( + <> +
+
+
Temperatur
+
{weather.current.temperature_2m} °C
+
+
+
Nedbør siste {weather.current.interval / 60} min
+
{weather.current.precipitation} mm
+
+
+
Vind
+
{weather.current.wind_speed_10m} m/s
+
+
+

+ Oppdatert{' '} + {' '} + (norsk tid). +

+ + )} +
+ ); +} + +function Header({ + city, + activity, + onCityChange, + onActivityChange, +}: { + city: City; + activity: Activity; + onCityChange: (city: City) => void; + onActivityChange: (activity: Activity) => void; +}) { + return ( +
+

IT2810 · Gruppe 31

+

Turvær Norge

+

Finn været for din neste tur.

+ + +
+ ); +} + +export function App() { + const [selectedCity, setSelectedCity] = useState(exampleCity); + const [selectedActivity, setSelectedActivity] = useState('Gåtur'); + const [weatherData, setWeatherData] = useState(null); + const weatherQueryResult = useQuery(weatherQuery(selectedCity)); + + useEffect(() => { + setWeatherData(weatherQueryResult.data ?? null); + }, [weatherQueryResult.data]); + + return ( +
+
+ void weatherQueryResult.refetch()} + /> + +
+ ); +} \ No newline at end of file diff --git a/web/src/WeatherExample.test.tsx b/web/src/WeatherExample.test.tsx index bc6021b..8d204a2 100644 --- a/web/src/WeatherExample.test.tsx +++ b/web/src/WeatherExample.test.tsx @@ -3,6 +3,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { http, HttpResponse } from 'msw'; +import { App } from './App'; import { WeatherExample } from './WeatherExample'; import { WEATHER_URL, fetchWeather } from './features/weather/weather'; import { exampleCity } from './features/weather/cities'; @@ -15,6 +16,13 @@ function renderExample() { , ); } +function renderApp() { + return render( + + + , + ); +} it('renders validated weather and a stable snapshot', async () => { const { asFragment } = renderExample(); await screen.findByText('14 °C'); @@ -25,6 +33,19 @@ it('renders validated weather and a stable snapshot', async () => { expect(screen.getByText('3 m/s')).toBeVisible(); expect(asFragment()).toMatchSnapshot(); }); +it('stores selected city and activity in App state', async () => { + renderApp(); + await screen.findByText('14 °C'); + + await userEvent.selectOptions(screen.getByRole('combobox', { name: 'By' }), 'oslo'); + await userEvent.selectOptions( + screen.getByRole('combobox', { name: 'Aktivitet' }), + 'Løpetur', + ); + + expect(screen.getByRole('heading', { name: 'Været i Oslo' })).toBeVisible(); + expect(screen.getByText('Valgt aktivitet: Løpetur')).toBeVisible(); +}); it('shows errors and retries on user interaction', async () => { server.use( http.get(WEATHER_URL, () => new HttpResponse(null, { status: 503 })), diff --git a/web/src/main.tsx b/web/src/main.tsx index 573102f..7ea3284 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,7 +1,7 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { WeatherExample } from './WeatherExample'; +import { App } from './App'; import './styles.css'; const root = document.getElementById('root'); if (!root) throw new Error('Missing root element'); @@ -9,7 +9,7 @@ const queryClient = new QueryClient(); createRoot(root).render( - + , ); From dc159e990fd8af1e44f3698e934938756271990c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20=C3=98rjavik?= Date: Sat, 12 Sep 2026 15:16:05 +0200 Subject: [PATCH 06/21] feature(weather): add WeatherList and WeatherCard Refs #6, #7 --- e2e/weather.spec.ts | 6 ++- web/src/App.tsx | 56 ++++++++++++++++----- web/src/WeatherCard.tsx | 88 +++++++++++++++++++++++++++++++++ web/src/WeatherExample.test.tsx | 12 ++++- web/src/WeatherList.tsx | 41 +++++++++++++++ web/src/styles.css | 27 ++++++++++ 6 files changed, 214 insertions(+), 16 deletions(-) create mode 100644 web/src/WeatherCard.tsx create mode 100644 web/src/WeatherList.tsx diff --git a/e2e/weather.spec.ts b/e2e/weather.spec.ts index 8586ca1..507e8a1 100644 --- a/e2e/weather.spec.ts +++ b/e2e/weather.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; +import { cities } from '../web/src/features/weather/cities'; import { weatherResponse } from '../web/src/test/fixtures'; test('weather example loads accessibly without horizontal overflow', async ({ page, @@ -14,7 +15,10 @@ test('weather example loads accessibly without horizontal overflow', async ({ page.getByRole('heading', { name: 'Været i Trondheim' }), ).toBeVisible(); await expect(page.getByText('14 °C')).toBeVisible(); - expect(calls).toBe(1); + await expect( + page.getByRole('heading', { name: 'Vær i norske byer' }), + ).toBeVisible(); + expect(calls).toBe(cities.length); expect( await page.evaluate( () => document.documentElement.scrollWidth <= window.innerWidth, diff --git a/web/src/App.tsx b/web/src/App.tsx index 33dcae6..585b8dc 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,7 +1,8 @@ -import { useEffect, useState } from 'react'; -import { useQuery } from '@tanstack/react-query'; +import { useState } from 'react'; +import { useQueries } from '@tanstack/react-query'; import { cities, exampleCity, type City } from './features/weather/cities'; import { weatherQuery, type CityWeather } from './features/weather/weather'; +import { WeatherList, type WeatherListItem } from './WeatherList'; const activities = ['Gåtur', 'Løpetur', 'Skitur'] as const; type Activity = (typeof activities)[number]; @@ -15,7 +16,14 @@ type WeatherContentProps = { onRetry: () => void; }; -function WeatherContent({ city, activity, weather, isLoading, error, onRetry }: WeatherContentProps) { +function WeatherContent({ + city, + activity, + weather, + isLoading, + error, + onRetry, +}: WeatherContentProps) { return (

Været i {city.name}

@@ -24,7 +32,9 @@ function WeatherContent({ city, activity, weather, isLoading, error, onRetry }: {error && (

Kunne ikke hente værdata. Prøv igjen om litt.

- +
)} {weather && ( @@ -108,12 +118,27 @@ function Header({ export function App() { const [selectedCity, setSelectedCity] = useState(exampleCity); const [selectedActivity, setSelectedActivity] = useState('Gåtur'); - const [weatherData, setWeatherData] = useState(null); - const weatherQueryResult = useQuery(weatherQuery(selectedCity)); + const weatherQueries = useQueries({ + queries: cities.map((city) => weatherQuery(city)), + }); + const selectedCityIndex = cities.findIndex( + ({ id }) => id === selectedCity.id, + ); + const weatherQueryResult = weatherQueries[selectedCityIndex]; + const weatherListItems: WeatherListItem[] = cities.map((city, index) => { + const query = weatherQueries[index]; + if (!query) { + return { city, weather: null, isLoading: true, hasError: false }; + } + return { + city, + weather: query.data ?? null, + isLoading: query.isPending, + hasError: query.isError, + }; + }); - useEffect(() => { - setWeatherData(weatherQueryResult.data ?? null); - }, [weatherQueryResult.data]); + const weatherData = weatherQueryResult?.data ?? null; return (
@@ -127,9 +152,14 @@ export function App() { city={selectedCity} activity={selectedActivity} weather={weatherData} - isLoading={weatherQueryResult.isPending} - error={weatherQueryResult.error} - onRetry={() => void weatherQueryResult.refetch()} + isLoading={weatherQueryResult?.isPending ?? true} + error={weatherQueryResult?.error ?? null} + onRetry={() => void weatherQueryResult?.refetch()} + /> +
Værdata fra Open-Meteo,{' '} @@ -137,4 +167,4 @@ export function App() {
); -} \ No newline at end of file +} diff --git a/web/src/WeatherCard.tsx b/web/src/WeatherCard.tsx new file mode 100644 index 0000000..cae4edb --- /dev/null +++ b/web/src/WeatherCard.tsx @@ -0,0 +1,88 @@ +import type { City } from './features/weather/cities'; +import type { CityWeather } from './features/weather/weather'; + +export type WeatherCardProps = { + city: City; + weather: CityWeather | null; + isLoading: boolean; + hasError: boolean; + isSelected: boolean; + onSelect: (city: City) => void; +}; + +const weatherTypes: Record = { + 0: 'Klarvær', + 1: 'Hovedsakelig klart', + 2: 'Delvis skyet', + 3: 'Overskyet', + 45: 'Tåke', + 48: 'Underkjølt tåke', + 51: 'Lett yr', + 53: 'Yr', + 55: 'Kraftig yr', + 61: 'Lett regn', + 63: 'Regn', + 65: 'Kraftig regn', + 71: 'Lett snø', + 73: 'Snø', + 75: 'Kraftig snø', + 80: 'Lette regnbyger', + 81: 'Regnbyger', + 82: 'Kraftige regnbyger', + 95: 'Tordenvær', + 96: 'Tordenvær med hagl', + 99: 'Tordenvær med kraftig hagl', +}; + +export function WeatherCard({ + city, + weather, + isLoading, + hasError, + isSelected, + onSelect, +}: WeatherCardProps) { + const weatherType = weather + ? (weatherTypes[weather.current.weather_code] ?? 'Ukjent værtype') + : null; + + return ( +
+

{city.name}

+ + {isLoading &&

Henter værdata…

} + {hasError &&

Kunne ikke hente værdata.

} + {weather && ( +
+
+
Værtype
+
{weatherType}
+
+
+
Temperatur
+
{weather.current.temperature_2m} °C
+
+
+
Nedbør
+
{weather.current.precipitation} mm
+
+
+
Vind
+
{weather.current.wind_speed_10m} m/s
+
+
+ )} +
+ ); +} diff --git a/web/src/WeatherExample.test.tsx b/web/src/WeatherExample.test.tsx index 8d204a2..0286749 100644 --- a/web/src/WeatherExample.test.tsx +++ b/web/src/WeatherExample.test.tsx @@ -35,9 +35,12 @@ it('renders validated weather and a stable snapshot', async () => { }); it('stores selected city and activity in App state', async () => { renderApp(); - await screen.findByText('14 °C'); + await screen.findByRole('heading', { name: 'Været i Trondheim' }); - await userEvent.selectOptions(screen.getByRole('combobox', { name: 'By' }), 'oslo'); + await userEvent.selectOptions( + screen.getByRole('combobox', { name: 'By' }), + 'oslo', + ); await userEvent.selectOptions( screen.getByRole('combobox', { name: 'Aktivitet' }), 'Løpetur', @@ -45,6 +48,11 @@ it('stores selected city and activity in App state', async () => { expect(screen.getByRole('heading', { name: 'Været i Oslo' })).toBeVisible(); expect(screen.getByText('Valgt aktivitet: Løpetur')).toBeVisible(); + + const bergenCard = screen.getByRole('button', { name: 'Vær i Bergen' }); + bergenCard.focus(); + await userEvent.keyboard('{Enter}'); + expect(screen.getByRole('heading', { name: 'Været i Bergen' })).toBeVisible(); }); it('shows errors and retries on user interaction', async () => { server.use( diff --git a/web/src/WeatherList.tsx b/web/src/WeatherList.tsx new file mode 100644 index 0000000..3904eee --- /dev/null +++ b/web/src/WeatherList.tsx @@ -0,0 +1,41 @@ +import type { City } from './features/weather/cities'; +import type { CityWeather } from './features/weather/weather'; +import { WeatherCard } from './WeatherCard'; + +export type WeatherListItem = { + city: City; + weather: CityWeather | null; + isLoading: boolean; + hasError: boolean; +}; + +export type WeatherListProps = { + items: WeatherListItem[]; + selectedCity: City; + onCitySelect: (city: City) => void; +}; + +export function WeatherList({ + items, + selectedCity, + onCitySelect, +}: WeatherListProps) { + return ( +
+

Vær i norske byer

+
+ {items.map((item) => ( + + ))} +
+
+ ); +} diff --git a/web/src/styles.css b/web/src/styles.css index 8943805..af2bf8f 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -27,6 +27,33 @@ section { padding: clamp(1rem, 4vw, 2rem); border-radius: 0.5rem; } +.weather-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); + gap: 1rem; +} +.weather-card { + border: 1px solid #bacac1; + border-radius: 0.5rem; + padding: 1rem; + background: #fff; + cursor: pointer; +} +.weather-card:hover, +.weather-card:focus-visible, +.weather-card-selected { + border-color: #173d35; + box-shadow: 0 0 0 3px #d5e2dc; +} +.weather-card h3 { + margin-top: 0; +} +.weather-card dl { + gap: 0.75rem 1rem; +} +.weather-card dd { + font-size: 1.1rem; +} dl { display: flex; flex-wrap: wrap; From a9f4fdd6088d87abf460490d87664d2ff1c6d1a1 Mon Sep 17 00:00:00 2001 From: Oliver Dragland Date: Mon, 14 Sep 2026 09:35:46 +0200 Subject: [PATCH 07/21] feature(weather): add loading, error and empty states --- docs/development.md | 2 + docs/testing.md | 10 ++ e2e/weather-states.spec.ts | 119 +++++++++++++++++++++++ web/src/WeatherExample.test.tsx | 146 ++++++++++++++++++++++++++-- web/src/WeatherExample.tsx | 44 ++++++--- web/src/features/weather/weather.ts | 12 ++- web/src/styles.css | 10 ++ 7 files changed, 320 insertions(+), 23 deletions(-) create mode 100644 e2e/weather-states.spec.ts diff --git a/docs/development.md b/docs/development.md index 0d0d844..286293c 100644 --- a/docs/development.md +++ b/docs/development.md @@ -12,6 +12,8 @@ The README covers everyday setup. This page contains optional infrastructure com Project 1 calls Open-Meteo directly. Wind is requested in m/s, timestamps use Europe/Oslo and precipitation is labelled with the provider's interval. Only the active response is held in memory; weather payloads are never persisted. Future storage features should store preferences and city IDs only. +`fetchWeather` returns `CityWeather | null`: HTTP 204, a JSON `null` response, or a validated response with a missing/null `current` observation means no weather is available. Partial observations, invalid units and malformed responses still fail Zod validation and produce a retryable error. Consumers must handle the empty result before reading weather fields. `WeatherExample` shows loading during initial requests and retries, explains requests paused while offline, and provides a retry button for errors and empty results. + Project 2 requires GraphQL and a database installed directly on the VM. Hosted Supabase and Docker backend/database deployment do not fit that requirement. Docker here is only an optional frontend development tool; Redis is not needed. ## Native PostgreSQL setup for Project 2 diff --git a/docs/testing.md b/docs/testing.md index b0c4ab0..721fa7d 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -15,6 +15,16 @@ Playwright smoke tests intercept provider data and check the production example, - `docker compose config --quiet`: passed. The optional frontend image/container was not built or started during this verification. - Verification toolchain: isolated Node 24.21.0 and npm 11.11.1. The machine's default Node/npm were not changed; follow README setup before installing locally. +## Loading, error and empty states — 2026-09-14 + +Codex assisted with the loading/error/empty-state implementation, regression tests and documentation. The changes were checked with an isolated Node 24.21.0 / npm 11.11.1 toolchain after `npm ci`. + +- `npm run check`: passed formatting, zero-warning lint, generated GraphQL contract verification, strict TypeScript, migration metadata, all 18 Vitest tests (15 frontend, 3 API), the unchanged success snapshot and both builds. +- Frontend tests cover a held loading response, HTTP failure with manual retry and loading feedback, no-content/null/missing observations, incomplete measurements, invalid JSON, network failure, and offline pause/resume. Invalid data never reaches the rendered weather fields. +- `npm run test:e2e`: all 25 tests passed in Chromium, Firefox, WebKit and emulated iPhone portrait/landscape. New checks cover pending requests, error semantics, keyboard retry, empty-result recovery, disabled duplicate retries and missing-field responses without uncaught page errors. Axe reported no violations in loading, error, empty or successful states. + +Automated tests use mocked provider responses. Real-device and screen-reader testing, live provider behavior, deployment and teammate review remain separate checks. + ## Remaining manual checks - Final client: navigation, activity assessment, filters, sort, favorites and both storage lifetimes. diff --git a/e2e/weather-states.spec.ts b/e2e/weather-states.spec.ts new file mode 100644 index 0000000..d63e995 --- /dev/null +++ b/e2e/weather-states.spec.ts @@ -0,0 +1,119 @@ +import { expect, test } from '@playwright/test'; +import AxeBuilder from '@axe-core/playwright'; +import { weatherResponse } from '../web/src/test/fixtures'; + +test('announces loading while the API request is pending', async ({ page }) => { + let finishRequest = () => {}; + const responseReady = new Promise((resolve) => { + finishRequest = resolve; + }); + await page.route('https://api.open-meteo.com/**', async (route) => { + await responseReady; + await route.fulfill({ json: weatherResponse }); + }); + await page.goto('./'); + try { + await expect(page.getByRole('status')).toHaveText('Henter værdata…'); + await expect(page.getByRole('alert')).toHaveCount(0); + expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]); + } finally { + finishRequest(); + } + await expect(page.getByText('14 °C')).toBeVisible(); + await expect(page.getByRole('status')).toHaveCount(0); +}); + +test('announces API errors and supports keyboard retry with loading feedback', async ({ + page, +}) => { + let calls = 0; + let finishRetry = () => {}; + const responseReady = new Promise((resolve) => { + finishRetry = resolve; + }); + await page.route('https://api.open-meteo.com/**', async (route) => { + calls++; + if (calls === 1) { + await route.fulfill({ status: 503 }); + return; + } + await responseReady; + await route.fulfill({ json: weatherResponse }); + }); + await page.goto('./'); + try { + await expect(page.getByRole('alert')).toContainText( + 'Kunne ikke hente værdata.', + ); + expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]); + expect(calls).toBe(1); + await page.keyboard.press('Tab'); + await expect( + page.getByRole('button', { name: 'Prøv igjen' }), + ).toBeFocused(); + await page.keyboard.press('Enter'); + await expect(page.getByRole('status')).toHaveText('Henter værdata…'); + await expect(page.getByRole('alert')).toHaveCount(0); + } finally { + finishRetry(); + } + await expect(page.getByText('14 °C')).toBeVisible(); + await expect(page.getByRole('status')).toHaveCount(0); + expect(calls).toBe(2); +}); + +test('explains empty results and disables repeat requests while retrying', async ({ + page, +}) => { + let calls = 0; + let finishRetry = () => {}; + const responseReady = new Promise((resolve) => { + finishRetry = resolve; + }); + await page.route('https://api.open-meteo.com/**', async (route) => { + calls++; + if (calls === 1) { + await route.fulfill({ status: 204 }); + return; + } + await responseReady; + await route.fulfill({ json: weatherResponse }); + }); + await page.goto('./'); + try { + await expect(page.getByRole('status')).toContainText( + 'Ingen værdata er tilgjengelige for Trondheim akkurat nå.', + ); + await expect(page.getByRole('alert')).toHaveCount(0); + expect((await new AxeBuilder({ page }).analyze()).violations).toEqual([]); + await page.getByRole('button', { name: 'Prøv igjen' }).click(); + await expect(page.getByRole('status')).toHaveText('Henter værdata…'); + await expect( + page.getByRole('button', { name: 'Prøv igjen' }), + ).toBeDisabled(); + } finally { + finishRetry(); + } + await expect(page.getByText('14 °C')).toBeVisible(); + await expect(page.getByRole('status')).toHaveCount(0); + expect(calls).toBe(2); +}); + +test('keeps the app usable when weather fields are missing', async ({ + page, +}) => { + const pageErrors: string[] = []; + page.on('pageerror', (error) => pageErrors.push(error.message)); + await page.route('https://api.open-meteo.com/**', async (route) => { + await route.fulfill({ json: { ...weatherResponse, current: {} } }); + }); + await page.goto('./'); + await expect(page.getByRole('alert')).toContainText( + 'Kunne ikke hente værdata.', + ); + await expect( + page.getByRole('heading', { name: 'Turvær Norge' }), + ).toBeVisible(); + await expect(page.getByRole('button', { name: 'Prøv igjen' })).toBeEnabled(); + expect(pageErrors).toEqual([]); +}); diff --git a/web/src/WeatherExample.test.tsx b/web/src/WeatherExample.test.tsx index bc6021b..1dfab6e 100644 --- a/web/src/WeatherExample.test.tsx +++ b/web/src/WeatherExample.test.tsx @@ -1,7 +1,11 @@ import { expect, it } from 'vitest'; -import { render, screen } from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + onlineManager, + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query'; import { http, HttpResponse } from 'msw'; import { WeatherExample } from './WeatherExample'; import { WEATHER_URL, fetchWeather } from './features/weather/weather'; @@ -25,15 +29,141 @@ it('renders validated weather and a stable snapshot', async () => { expect(screen.getByText('3 m/s')).toBeVisible(); expect(asFragment()).toMatchSnapshot(); }); -it('shows errors and retries on user interaction', async () => { +it('shows a loading message until the response arrives', async () => { + let finishRequest = () => {}; + const responseReady = new Promise((resolve) => { + finishRequest = resolve; + }); server.use( - http.get(WEATHER_URL, () => new HttpResponse(null, { status: 503 })), + http.get(WEATHER_URL, async () => { + await responseReady; + return HttpResponse.json(weatherResponse); + }), + ); + renderExample(); + try { + expect(screen.getByRole('status')).toHaveTextContent('Henter værdata…'); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + } finally { + finishRequest(); + } + await screen.findByText('14 °C'); + expect(screen.queryByRole('status')).not.toBeInTheDocument(); +}); +it('shows an accessible error and loading feedback during a manual retry', async () => { + let calls = 0; + let finishRetry = () => {}; + const responseReady = new Promise((resolve) => { + finishRetry = resolve; + }); + server.use( + http.get(WEATHER_URL, async () => { + calls++; + if (calls === 1) return new HttpResponse(null, { status: 503 }); + await responseReady; + return HttpResponse.json(weatherResponse); + }), ); renderExample(); - await screen.findByRole('alert'); - server.resetHandlers(); - await userEvent.click(screen.getByRole('button', { name: 'Prøv igjen' })); + try { + expect(await screen.findByRole('alert')).toHaveTextContent( + 'Kunne ikke hente værdata.', + ); + expect(calls).toBe(1); + await userEvent.click(screen.getByRole('button', { name: 'Prøv igjen' })); + expect(await screen.findByRole('status')).toHaveTextContent( + 'Henter værdata…', + ); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + } finally { + finishRetry(); + } await screen.findByText('14 °C'); + expect(calls).toBe(2); + expect(screen.queryByRole('status')).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: 'Prøv igjen' }), + ).not.toBeInTheDocument(); +}); +it.each([ + ['HTTP 204', () => new HttpResponse(null, { status: 204 })], + ['null response', () => HttpResponse.json(null)], + [ + 'null observation', + () => HttpResponse.json({ ...weatherResponse, current: null }), + ], + [ + 'missing observation', + () => + HttpResponse.json({ + timezone: weatherResponse.timezone, + current_units: weatherResponse.current_units, + }), + ], +])( + 'shows an empty state for %s and allows a new request', + async (_name, response) => { + server.use(http.get(WEATHER_URL, response)); + renderExample(); + await screen.findByText( + 'Ingen værdata er tilgjengelige for Trondheim akkurat nå. Prøv igjen om litt.', + ); + expect(screen.getByRole('status')).toHaveTextContent('Ingen værdata'); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + expect(screen.queryByRole('definition')).not.toBeInTheDocument(); + server.resetHandlers(); + await userEvent.click(screen.getByRole('button', { name: 'Prøv igjen' })); + await screen.findByText('14 °C'); + expect(screen.queryByRole('status')).not.toBeInTheDocument(); + }, +); +it.each([ + ['missing fields', () => HttpResponse.json({})], + [ + 'partial observation', + () => + HttpResponse.json({ + ...weatherResponse, + current: { temperature_2m: 14 }, + }), + ], + [ + 'null measurement', + () => + HttpResponse.json({ + ...weatherResponse, + current: { ...weatherResponse.current, temperature_2m: null }, + }), + ], + ['invalid JSON', () => new HttpResponse('{')], + ['network failure', () => HttpResponse.error()], +])( + 'handles %s without crashing and allows recovery', + async (_name, response) => { + server.use(http.get(WEATHER_URL, response)); + renderExample(); + expect(await screen.findByRole('alert')).toHaveTextContent( + 'Kunne ikke hente værdata.', + ); + expect(screen.getByRole('heading', { name: 'Turvær Norge' })).toBeVisible(); + expect(screen.queryByRole('definition')).not.toBeInTheDocument(); + server.resetHandlers(); + await userEvent.click(screen.getByRole('button', { name: 'Prøv igjen' })); + await screen.findByText('14 °C'); + }, +); +it('explains a paused offline request and resumes when connected', async () => { + onlineManager.setOnline(false); + try { + renderExample(); + expect(screen.getByRole('status')).toHaveTextContent('Du er frakoblet.'); + act(() => { + onlineManager.setOnline(true); + }); + await screen.findByText('14 °C'); + } finally { + onlineManager.setOnline(true); + } }); it('rejects malformed provider data and unexpected units', async () => { server.use( @@ -60,7 +190,7 @@ it('includes coordinates, explicit units, and cancellation in requests', async ( return HttpResponse.json(weatherResponse); }), ); - expect((await fetchWeather(exampleCity)).city.id).toBe('trondheim'); + expect((await fetchWeather(exampleCity))?.city.id).toBe('trondheim'); const controller = new AbortController(); controller.abort(); await expect(fetchWeather(exampleCity, controller.signal)).rejects.toThrow(); diff --git a/web/src/WeatherExample.tsx b/web/src/WeatherExample.tsx index 3045ab4..873a17b 100644 --- a/web/src/WeatherExample.tsx +++ b/web/src/WeatherExample.tsx @@ -14,19 +14,37 @@ export function WeatherExample() {

Været i {exampleCity.name}

- {weather.isPending &&

Henter værdata…

} - {weather.isError && ( -
-

Kunne ikke hente værdata. Prøv igjen om litt.

- -
+ {weather.isFetching &&

Henter værdata…

} + {weather.isPaused && ( +

+ Du er frakoblet. Venter på nettforbindelse for å hente værdata. +

+ )} + {weather.isError && !weather.isFetching && !weather.isPaused && ( +

+ Kunne ikke hente værdata. Sjekk nettforbindelsen din eller prøv + igjen om litt. +

+ )} + {weather.isSuccess && + !weather.data && + !weather.isFetching && + !weather.isPaused && ( +

+ Ingen værdata er tilgjengelige for {exampleCity.name} akkurat nå. + Prøv igjen om litt. +

+ )} + {(weather.isError || (weather.isSuccess && !weather.data)) && ( + )} {weather.data && ( <> diff --git a/web/src/features/weather/weather.ts b/web/src/features/weather/weather.ts index ef440b6..b755fc6 100644 --- a/web/src/features/weather/weather.ts +++ b/web/src/features/weather/weather.ts @@ -21,6 +21,12 @@ export const weatherResponseSchema = z.object({ }); export type WeatherResponse = z.infer; +// A successful response may contain no current observation. Partial observations +// still fail validation instead of reaching the UI with missing fields. +const weatherResultSchema = weatherResponseSchema + .extend({ current: weatherResponseSchema.shape.current.nullish() }) + .nullable(); + export async function fetchWeather(city: City, signal?: AbortSignal) { const url = new URL(WEATHER_URL); url.search = new URLSearchParams({ @@ -38,11 +44,13 @@ export async function fetchWeather(city: City, signal?: AbortSignal) { }); if (!response.ok) throw new Error(`Weather API returned HTTP ${response.status}`); + if (response.status === 204) return null; const body: unknown = await response.json(); - const parsed = weatherResponseSchema.parse(body); + const parsed = weatherResultSchema.parse(body); + if (!parsed?.current) return null; return { city, current: parsed.current }; } -export type CityWeather = Awaited>; +export type CityWeather = NonNullable>>; export function weatherQuery(city: City) { return queryOptions({ diff --git a/web/src/styles.css b/web/src/styles.css index 8943805..9656e8e 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -50,6 +50,16 @@ button { border-radius: 0.25rem; cursor: pointer; } +button:disabled { + opacity: 0.65; + cursor: wait; +} +[role='alert'] { + padding: 0.75rem 1rem; + border-left: 4px solid #9a491c; + background: #fff3ec; + color: #673514; +} a { color: inherit; text-underline-offset: 0.2em; From 2c022f6dd678d644ddfc5336223063bf7b571290 Mon Sep 17 00:00:00 2001 From: Oliver Dragland Date: Mon, 14 Sep 2026 09:49:12 +0200 Subject: [PATCH 08/21] feature(ui): share query feedback components --- README.md | 2 +- docs/development.md | 3 +- docs/testing.md | 7 ++ docs/ui-feedback.md | 65 ++++++++++++++ web/src/WeatherExample.tsx | 44 +++------ .../feedback/FeedbackMessage.test.tsx | 79 ++++++++++++++++ .../components/feedback/FeedbackMessage.tsx | 69 ++++++++++++++ .../feedback/QueryFeedback.test.tsx | 89 +++++++++++++++++++ web/src/components/feedback/QueryFeedback.tsx | 57 ++++++++++++ web/src/components/feedback/feedback.css | 37 ++++++++ web/src/components/feedback/index.ts | 9 ++ web/src/styles.css | 20 ----- 12 files changed, 427 insertions(+), 54 deletions(-) create mode 100644 docs/ui-feedback.md create mode 100644 web/src/components/feedback/FeedbackMessage.test.tsx create mode 100644 web/src/components/feedback/FeedbackMessage.tsx create mode 100644 web/src/components/feedback/QueryFeedback.test.tsx create mode 100644 web/src/components/feedback/QueryFeedback.tsx create mode 100644 web/src/components/feedback/feedback.css create mode 100644 web/src/components/feedback/index.ts diff --git a/README.md b/README.md index ebeaa0a..0efd558 100644 --- a/README.md +++ b/README.md @@ -122,4 +122,4 @@ Velg et issue før du starter, og lenk det i PR-en, for eksempel `Closes #8`. Å ## Dokumentasjon -[Roadmap](roadmap.md) · [Utviklerguide og planlagt dataflyt](docs/development.md) · [Krav og gjenstående arbeid](docs/requirements.md) · [Tester](docs/testing.md) +[Roadmap](roadmap.md) · [Utviklerguide og planlagt dataflyt](docs/development.md) · [Delte statuskomponenter](docs/ui-feedback.md) · [Krav og gjenstående arbeid](docs/requirements.md) · [Tester](docs/testing.md) diff --git a/docs/development.md b/docs/development.md index 286293c..6781b81 100644 --- a/docs/development.md +++ b/docs/development.md @@ -5,6 +5,7 @@ The README covers everyday setup. This page contains optional infrastructure com ## Structure and type safety - `web/src/WeatherExample.tsx`: the single weather example. +- `web/src/components/feedback/`: shared loading, error, empty and offline messages plus a typed TanStack Query adapter; see [usage and extension](ui-feedback.md). - `web/src/features/weather/`: typed city definitions, Zod response validation and TanStack Query options. - `web/src/shared/storage.ts`: storage helper for future preferences and favorite city IDs. - `project2/api/`: independent Node/Hono/GraphQL API with native PostgreSQL and Drizzle migrations. @@ -79,7 +80,7 @@ Project 2's eventual client belongs at `/project2/`. Run its built API on Node, ## Planned Project 1 structure -The following diagrams are retained from the team's project plan; these components are not implemented by the skeleton. +The following diagrams are retained from the team's project plan. Shared loading/error feedback is implemented; the full app, list and details composition remains planned. ## Dataflyt diff --git a/docs/testing.md b/docs/testing.md index 721fa7d..eab9dad 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -25,6 +25,13 @@ Codex assisted with the loading/error/empty-state implementation, regression tes Automated tests use mocked provider responses. Real-device and screen-reader testing, live provider behavior, deployment and teammate review remain separate checks. +## Shared feedback components — 2026-09-14 + +Codex assisted with extracting standalone loading, error, empty and offline messages, a typed TanStack Query adapter, scoped CSS, component tests and the [usage guide](ui-feedback.md). The weather example now consumes the shared components. Component tests cover standalone use, keyboard actions, disabled actions, independent instances, custom empty-list rules, valid falsy data and disabled queries. + +- `npm run check`: passed formatting, zero-warning lint, generated contracts, strict TypeScript, migration metadata, all 27 Vitest tests (24 frontend, 3 API), the unchanged success snapshot and both builds, using Node 24.21.0 and npm 11.11.1. +- `npm run test:e2e`: all 25 tests passed with the shared components in Chromium, Firefox, WebKit and emulated iPhone portrait/landscape. This includes keyboard retry, state transitions, empty-result recovery, malformed data, overflow and axe accessibility checks. Axe found no violations in the tested loading, error, empty and success states. + ## Remaining manual checks - Final client: navigation, activity assessment, filters, sort, favorites and both storage lifetimes. diff --git a/docs/ui-feedback.md b/docs/ui-feedback.md new file mode 100644 index 0000000..3af5652 --- /dev/null +++ b/docs/ui-feedback.md @@ -0,0 +1,65 @@ +# Shared UI feedback + +Import feedback components from `web/src/components/feedback`. They are self-authored React components with plain CSS and no additional runtime dependencies. `WeatherExample` is the working integration. + +## Standalone messages + +| Component | Default message | Semantics | +| ---------------- | ------------------------------------------- | ------------- | +| `LoadingMessage` | Laster… | `role=status` | +| `ErrorMessage` | Noe gikk galt. Prøv igjen om litt. | `role=alert` | +| `EmptyMessage` | Ingen resultater. | `role=status` | +| `OfflineMessage` | Du er frakoblet. Venter på nettforbindelse. | `role=status` | + +Each component accepts a custom `message` string and an optional `action` with `label`, `onClick` and `disabled`. They work without TanStack Query or a provider, so features can use them for local filters, favorites and other UI state. + +For example, inside a feature with a `resetFilters` callback: + +```tsx +import { EmptyMessage } from './components/feedback'; + +; +``` + +Actions use native buttons with an explicit `type="button"`, visible keyboard focus and a minimum 44px height. Disabled actions cannot be activated. The button sits outside the live message region, so the alert contains only the explanation. Roles are fixed by the component to keep semantics consistent across features. + +## TanStack Query integration + +`QueryFeedback` selects a message from an existing typed query result. It does not fetch data, create a query client, render results or change query options. Features continue to own query keys, live REST requests, Zod validation and data presentation. + +For a feature with an existing `citiesQuery` whose successful data is an array: + +```tsx +import { QueryFeedback } from './components/feedback'; + + cities.length === 0} + messages={{ + loading: 'Henter byer…', + error: 'Kunne ikke hente byene. Prøv igjen om litt.', + empty: 'Ingen byer er tilgjengelige akkurat nå.', + }} + retryLabel="Hent byer på nytt" +/>; +``` + +- `query`: the existing query result; data and error types are inferred. +- `messages`: optional overrides for `loading`, `error`, `empty` and `offline`. Omitted values use the standalone defaults. Use understandable, feature-specific text rather than raw provider errors. +- `isEmpty`: optional domain rule. Null/undefined data is always empty and is never passed to the predicate; valid values such as `0` and `false` are not empty. Empty arrays require an explicit length check. +- `retryLabel`: accessible button text, defaulting to `Prøv igjen`. Errors and empty responses offer a manual `refetch`; retry buttons are disabled while a request is fetching or paused. + +Paused and fetching states take precedence over previous errors or empty results. A disabled query that is pending but idle shows no loading message. Successful nonempty data shows no feedback; the feature renders its own data alongside `QueryFeedback`. + +Use one adapter for a list loaded by one query. Use per-card feedback when cards fetch independently, and include the city name in messages and retry labels so users can distinguish actions. For client-side filtering, use `EmptyMessage` with an action that clears filters rather than refetching unchanged server data. + +## Styling and extension + +The components import `feedback.css` themselves. All selectors are scoped under `feedback-message`, so unrelated alerts and buttons keep their own styles. Wrapping text, logical spacing and native button sizing allow the same components to fit cards and full-page sections. + +Add feature wording and behavior through props. Change common spacing, colors or focus styles in the shared stylesheet. Keep weather types, API URLs and feature-specific state out of the standalone components. The adapter reads TanStack Query state directly; it does not duplicate that state in effects or component state. + +Component tests cover standalone use, keyboard actions, disabled actions, independent instances, custom empty-list rules, valid falsy data and disabled queries. The weather integration tests and Playwright suite cover the full request/retry lifecycle and accessibility checks. diff --git a/web/src/WeatherExample.tsx b/web/src/WeatherExample.tsx index 873a17b..2e3c417 100644 --- a/web/src/WeatherExample.tsx +++ b/web/src/WeatherExample.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import { QueryFeedback } from './components/feedback'; import { exampleCity } from './features/weather/cities'; import { weatherQuery } from './features/weather/weather'; @@ -14,38 +15,17 @@ export function WeatherExample() {

Været i {exampleCity.name}

- {weather.isFetching &&

Henter værdata…

} - {weather.isPaused && ( -

- Du er frakoblet. Venter på nettforbindelse for å hente værdata. -

- )} - {weather.isError && !weather.isFetching && !weather.isPaused && ( -

- Kunne ikke hente værdata. Sjekk nettforbindelsen din eller prøv - igjen om litt. -

- )} - {weather.isSuccess && - !weather.data && - !weather.isFetching && - !weather.isPaused && ( -

- Ingen værdata er tilgjengelige for {exampleCity.name} akkurat nå. - Prøv igjen om litt. -

- )} - {(weather.isError || (weather.isSuccess && !weather.data)) && ( - - )} + {weather.data && ( <>
diff --git a/web/src/components/feedback/FeedbackMessage.test.tsx b/web/src/components/feedback/FeedbackMessage.test.tsx new file mode 100644 index 0000000..45037ab --- /dev/null +++ b/web/src/components/feedback/FeedbackMessage.test.tsx @@ -0,0 +1,79 @@ +import { expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { + EmptyMessage, + ErrorMessage, + LoadingMessage, + OfflineMessage, +} from './FeedbackMessage'; + +it('renders standalone status messages without a query provider', () => { + render( + <> + + + , + ); + const statuses = screen.getAllByRole('status'); + expect(statuses[0]).toHaveTextContent('Henter favoritter…'); + expect(statuses[1]).toHaveTextContent('Du er frakoblet.'); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); +}); + +it('provides an accessible error with a keyboard action outside the live region', async () => { + const onRetry = vi.fn(); + const user = userEvent.setup(); + render( + , + ); + const alert = screen.getByRole('alert'); + const button = screen.getByRole('button', { name: 'Prøv Bergen igjen' }); + expect(alert).toHaveTextContent('Noe gikk galt. Prøv igjen om litt.'); + expect(alert).not.toContainElement(button); + expect(button).toHaveAttribute('type', 'button'); + await user.tab(); + expect(button).toHaveFocus(); + await user.keyboard('{Enter}'); + expect(onRetry).toHaveBeenCalledOnce(); +}); + +it('supports a custom empty-result action and prevents disabled actions', async () => { + const onReset = vi.fn(); + const action = { label: 'Nullstill filtre', onClick: onReset }; + const { rerender } = render( + , + ); + expect(screen.getByRole('status')).toHaveTextContent( + 'Ingen byer passer filtrene dine.', + ); + await userEvent.click(screen.getByRole('button', { name: action.label })); + expect(onReset).toHaveBeenCalledOnce(); + rerender(); + expect(screen.getByRole('button', { name: action.label })).toBeDisabled(); + await userEvent.click(screen.getByRole('button', { name: action.label })); + expect(onReset).toHaveBeenCalledOnce(); +}); + +it('keeps messages and actions independent when several cards need feedback', async () => { + const retryBergen = vi.fn(); + const retryOslo = vi.fn(); + render( + <> + + + , + ); + expect(screen.getAllByRole('alert')).toHaveLength(2); + await userEvent.click( + screen.getByRole('button', { name: 'Prøv Bergen igjen' }), + ); + expect(retryBergen).toHaveBeenCalledOnce(); + expect(retryOslo).not.toHaveBeenCalled(); +}); diff --git a/web/src/components/feedback/FeedbackMessage.tsx b/web/src/components/feedback/FeedbackMessage.tsx new file mode 100644 index 0000000..10101e9 --- /dev/null +++ b/web/src/components/feedback/FeedbackMessage.tsx @@ -0,0 +1,69 @@ +import './feedback.css'; + +export type FeedbackAction = { + label: string; + onClick: () => void; + disabled?: boolean; +}; + +export type FeedbackMessageProps = { + message?: string | undefined; + action?: FeedbackAction | undefined; +}; + +type FeedbackKind = 'loading' | 'error' | 'empty' | 'offline'; + +function FeedbackMessage({ + kind, + message, + action, +}: FeedbackMessageProps & { kind: FeedbackKind; message: string }) { + return ( +
+

+ {message} +

+ {action && ( + + )} +
+ ); +} + +export function LoadingMessage({ + message = 'Laster…', + action, +}: FeedbackMessageProps) { + return ; +} + +export function ErrorMessage({ + message = 'Noe gikk galt. Prøv igjen om litt.', + action, +}: FeedbackMessageProps) { + return ; +} + +export function EmptyMessage({ + message = 'Ingen resultater.', + action, +}: FeedbackMessageProps) { + return ; +} + +export function OfflineMessage({ + message = 'Du er frakoblet. Venter på nettforbindelse.', + action, +}: FeedbackMessageProps) { + return ; +} diff --git a/web/src/components/feedback/QueryFeedback.test.tsx b/web/src/components/feedback/QueryFeedback.test.tsx new file mode 100644 index 0000000..9c865c3 --- /dev/null +++ b/web/src/components/feedback/QueryFeedback.test.tsx @@ -0,0 +1,89 @@ +import { expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { + QueryClient, + QueryClientProvider, + useQuery, +} from '@tanstack/react-query'; +import { QueryFeedback } from './QueryFeedback'; + +function QueryExample({ + load, + isEmpty, + enabled = true, +}: { + load: () => Promise; + isEmpty?: (data: NonNullable) => boolean; + enabled?: boolean; +}) { + const query = useQuery({ + queryKey: ['feedback-example'], + queryFn: load, + enabled, + retry: false, + gcTime: 0, + }); + return ( + <> + + {query.isSuccess && ( + {JSON.stringify(query.data)} + )} + + ); +} + +it('supports list-specific emptiness and a manual retry through TanStack Query', async () => { + const load = vi + .fn<() => Promise>() + .mockResolvedValueOnce([]) + .mockResolvedValue(['Bergen']); + render( + + cities.length === 0} /> + , + ); + await screen.findByText('Ingen byer funnet.'); + await userEvent.click( + screen.getByRole('button', { name: 'Hent byer på nytt' }), + ); + await screen.findByText('["Bergen"]'); + expect(screen.queryByText('Ingen byer funnet.')).not.toBeInTheDocument(); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + expect(load).toHaveBeenCalledTimes(2); +}); + +it.each([0, false, ''])( + 'does not mistake the valid value %j for an empty result', + async (value) => { + render( + + Promise.resolve(value)} /> + , + ); + expect(await screen.findByLabelText('Resultat')).toHaveTextContent( + JSON.stringify(value), + ); + expect(screen.queryByText('Ingen byer funnet.')).not.toBeInTheDocument(); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }, +); + +it('does not show a loading message or start a disabled query', () => { + const load = vi.fn<() => Promise>().mockResolvedValue([]); + render( + + + , + ); + expect(screen.queryByRole('status')).not.toBeInTheDocument(); + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + expect(load).not.toHaveBeenCalled(); +}); diff --git a/web/src/components/feedback/QueryFeedback.tsx b/web/src/components/feedback/QueryFeedback.tsx new file mode 100644 index 0000000..adcba52 --- /dev/null +++ b/web/src/components/feedback/QueryFeedback.tsx @@ -0,0 +1,57 @@ +import type { UseQueryResult } from '@tanstack/react-query'; +import { + EmptyMessage, + ErrorMessage, + LoadingMessage, + OfflineMessage, + type FeedbackAction, +} from './FeedbackMessage'; + +export type QueryFeedbackProps = { + query: Pick< + UseQueryResult, + 'data' | 'status' | 'fetchStatus' | 'refetch' + >; + messages?: Partial>; + /** Null/undefined are always empty; supply domain rules such as list.length === 0. */ + isEmpty?: ((data: NonNullable) => boolean) | undefined; + retryLabel?: string; +}; + +/** Renders query feedback only; the feature owns its data view and query options. */ +export function QueryFeedback({ + query, + messages = {}, + isEmpty, + retryLabel = 'Prøv igjen', +}: QueryFeedbackProps) { + const empty = + query.status === 'success' && + (query.data == null || isEmpty?.(query.data) === true); + const action: FeedbackAction | undefined = + query.status === 'error' || empty + ? { + label: retryLabel, + disabled: query.fetchStatus !== 'idle', + onClick: () => { + void query.refetch(); + }, + } + : undefined; + + // Paused/fetching states take precedence over the result of an earlier request. + if (query.fetchStatus === 'paused') { + return ; + } + if (query.fetchStatus === 'fetching') { + return ; + } + if (query.status === 'error') { + return ; + } + if (empty) { + return ; + } + // Includes disabled queries (pending but idle), which are not loading. + return null; +} diff --git a/web/src/components/feedback/feedback.css b/web/src/components/feedback/feedback.css new file mode 100644 index 0000000..e997c76 --- /dev/null +++ b/web/src/components/feedback/feedback.css @@ -0,0 +1,37 @@ +.feedback-message { + margin-block: 1rem; + overflow-wrap: anywhere; +} + +.feedback-message__text { + margin: 0; +} + +.feedback-message--error .feedback-message__text { + padding: 0.75rem 1rem; + border-inline-start: 4px solid #9a491c; + background: #fff3ec; + color: #673514; +} + +.feedback-message__action { + margin-block-start: 0.75rem; + font: inherit; + min-height: 44px; + padding: 0.5rem 1rem; + background: #173d35; + color: white; + border: 0; + border-radius: 0.25rem; + cursor: pointer; +} + +.feedback-message__action:disabled { + opacity: 0.65; + cursor: wait; +} + +.feedback-message__action:focus-visible { + outline: 3px solid #9a491c; + outline-offset: 4px; +} diff --git a/web/src/components/feedback/index.ts b/web/src/components/feedback/index.ts new file mode 100644 index 0000000..66f48ab --- /dev/null +++ b/web/src/components/feedback/index.ts @@ -0,0 +1,9 @@ +export { + EmptyMessage, + ErrorMessage, + LoadingMessage, + OfflineMessage, + type FeedbackAction, + type FeedbackMessageProps, +} from './FeedbackMessage'; +export { QueryFeedback, type QueryFeedbackProps } from './QueryFeedback'; diff --git a/web/src/styles.css b/web/src/styles.css index 9656e8e..ed731af 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -40,26 +40,6 @@ dd { font-size: 1.5rem; font-weight: 600; } -button { - font: inherit; - min-height: 44px; - padding: 0.5rem 1rem; - background: #173d35; - color: white; - border: 0; - border-radius: 0.25rem; - cursor: pointer; -} -button:disabled { - opacity: 0.65; - cursor: wait; -} -[role='alert'] { - padding: 0.75rem 1rem; - border-left: 4px solid #9a491c; - background: #fff3ec; - color: #673514; -} a { color: inherit; text-underline-offset: 0.2em; From 76c16b1374a65b38ae00fec00f380399fdbced52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20=C3=98rjavik?= Date: Mon, 14 Sep 2026 11:13:59 +0200 Subject: [PATCH 09/21] feature(weather): add ranked cards and city details Refs #7, #8, #9, #11, #12 --- docs/weather-app-architecture.md | 74 ++++++ docs/weather-cards-and-ranking.md | 66 +++++ e2e/weather.spec.ts | 4 + web/src/App.tsx | 423 ++++++++++++++++++++++-------- web/src/WeatherCard.tsx | 31 +++ web/src/WeatherExample.test.tsx | 134 +++++++++- web/src/WeatherList.tsx | 41 ++- web/src/styles.css | 161 +++++++++++- 8 files changed, 808 insertions(+), 126 deletions(-) create mode 100644 docs/weather-app-architecture.md create mode 100644 docs/weather-cards-and-ranking.md diff --git a/docs/weather-app-architecture.md b/docs/weather-app-architecture.md new file mode 100644 index 0000000..4ee02b8 --- /dev/null +++ b/docs/weather-app-architecture.md @@ -0,0 +1,74 @@ +# Weather app architecture + +## Overview + +The production entry point is `web/src/App.tsx`. `App` owns the user-facing state and composes the reusable city list: + +```text +App +├── Header +│ ├── city selector +│ ├── activity selector +│ └── sort selector +└── WeatherList + └── WeatherCard + └── MainWeather (centered overlay while that card is expanded) +``` + +`WeatherExample.tsx` remains a small API example used by the existing provider tests. The production page uses all configured cities and TanStack Query. + +## State and persistence + +- The selected city is stored in `sessionStorage` under `t31-selected-city`. +- The selected activity is stored in `sessionStorage` under `t31-activity`. +- The selected sort mode is stored in `sessionStorage` under `t31-sort-mode`. +- Favorite city IDs are stored in `localStorage` under `t31-favorite-cities`. +- `useStoredState` parses JSON and validates stored values with Zod. Invalid or unavailable values fall back to defaults. +- Weather payloads are not stored in Web Storage. TanStack Query owns the live API responses. + +Session storage is used for choices that describe the current visit. Local storage is used for favorites because favorites should survive closing and reopening the browser. + +## Main view and navigation + +The default view presents the weather cards first. Users can select a city through the city select or a card. A card's details action opens a wider centered overlay without changing the grid row height or aligning the panel to the card column. The panel supplies the main temperature, weather type, precipitation, wind, update time and provider interval, with a close action. + +The card list remains visible around the expanded detail. It is sorted from a copied array, so the source city list is never mutated. + +## Sorting and ranking + +The list supports: + +- Original city order. +- Best weather for the selected activity. +- Highest temperature. +- Lowest precipitation. +- Lowest wind speed. + +The activity ranking is intentionally small and explainable at this stage. It rewards conditions suitable for the selected activity and subtracts penalties for precipitation and wind. Ranking is presentation logic; the validated API data remains unchanged. + +## Accessibility and responsive design + +- Native `select` and `button` controls provide keyboard interaction. +- Labels describe all selection controls. +- `main`, `header`, `section`, `article`, `dl`, `dt` and `dd` provide semantic structure. +- Loading and API failures use `role="status"` and `role="alert"`. +- Focus-visible styles are defined in `web/src/styles.css`. +- The layout changes from a horizontal desktop header to stacked mobile controls below 760px. +- Cards use stable grid sizing and remain usable in narrow viewports. + +## Verification + +Checks run for this implementation: + +```bash +npm test +npm run lint +npm run format:check +npm run build --workspace @t31/web +``` + +The component tests use MSW fixtures, so unit tests do not call Open-Meteo. They cover the existing validated weather example, city/activity state, city navigation, sorting persistence and favorite persistence. Playwright remains the browser-level check for responsive layout and accessibility; install its browsers with `npm run test:e2e:install` before running it locally. + +## Scope notes + +The ranking formula is a first usable implementation, not a medical or safety forecast. Future issues can refine activity-specific thresholds, add a visible ranking explanation, and add filtering for favorites or weather categories. diff --git a/docs/weather-cards-and-ranking.md b/docs/weather-cards-and-ranking.md new file mode 100644 index 0000000..b7b75c1 --- /dev/null +++ b/docs/weather-cards-and-ranking.md @@ -0,0 +1,66 @@ +# Weather cards and activity ranking + +## New page flow + +The weather cards are now the default main view. The page opens directly on the city collection so users can compare the available cities at a glance. + +Each card provides: + +- city name +- weather type +- temperature +- precipitation +- wind +- city selection +- favorite toggle +- a `Se detaljer` action + +Selecting a city updates the selected city state. Opening details shows a wider centered overlay. The overlay is independent of the card column, does not change the height of the card grid, and contains the selected city's relevant weather summary and update time. It can be closed with the close button or by clicking outside the panel. + +## Activity-specific ranking + +The `Best vær for aktivitet` sort option uses a separate weather profile for each activity: + +- `Gåtur` prefers mild temperatures, low precipitation and moderate wind. +- `Løpetur` prefers cooler temperatures, no precipitation and low wind. +- `Skitur` prefers temperatures below freezing and gives a bonus to snow weather codes. + +The score combines distance from the activity's ideal temperature with precipitation and wind penalties. The resulting ranking is presentation state only; API responses and the city source array are not changed. + +When the best-activity sort is active, cards display their current rank. Changing the activity recalculates all scores and can change which city is ranked first. + +## State and storage + +`App` owns the selected city, activity, sort mode, favorite IDs and currently expanded detail city: + +- selected city, activity and sort mode use `sessionStorage` +- favorite city IDs use `localStorage` +- expanded detail state is temporary React state and is not persisted +- weather payloads remain in TanStack Query rather than Web Storage + +Stored values are parsed and validated with Zod through `useStoredState`. Invalid storage values fall back to safe defaults. + +## Accessibility and responsive behavior + +The implementation uses native `select` and `button` controls, accessible labels, semantic headings and `dl` elements for weather values. The expanded main view has a clear heading and close control. Focus-visible styles remain available for keyboard users, and the card grid/control area stacks on narrow screens. + +## Verification + +The implementation is covered by MSW-backed Vitest tests for: + +- the existing validated weather example +- city and activity selection +- card-first detail expansion and navigation +- favorite and sort persistence +- activity-specific ranking changes + +The standard checks are: + +```bash +npm test +npm run lint +npm run format:check +npm run build --workspace @t31/web +``` + +Playwright browser checks should be run after installing the configured browsers with `npm run test:e2e:install`. diff --git a/e2e/weather.spec.ts b/e2e/weather.spec.ts index 507e8a1..94bad8d 100644 --- a/e2e/weather.spec.ts +++ b/e2e/weather.spec.ts @@ -11,6 +11,10 @@ test('weather example loads accessibly without horizontal overflow', async ({ await route.fulfill({ json: weatherResponse }); }); await page.goto('./'); + await expect( + page.getByRole('heading', { name: 'Vær i norske byer' }), + ).toBeVisible(); + await page.getByRole('button', { name: 'Se detaljer for Trondheim' }).click(); await expect( page.getByRole('heading', { name: 'Været i Trondheim' }), ).toBeVisible(); diff --git a/web/src/App.tsx b/web/src/App.tsx index 585b8dc..f6f1873 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,37 +1,197 @@ -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { useQueries } from '@tanstack/react-query'; -import { cities, exampleCity, type City } from './features/weather/cities'; +import { z } from 'zod'; +import { + cities, + exampleCity, + type City, + type CityId, +} from './features/weather/cities'; import { weatherQuery, type CityWeather } from './features/weather/weather'; +import { useStoredState } from './shared/storage'; import { WeatherList, type WeatherListItem } from './WeatherList'; const activities = ['Gåtur', 'Løpetur', 'Skitur'] as const; type Activity = (typeof activities)[number]; +const sortModes = ['default', 'best', 'temperature', 'rain', 'wind'] as const; +type SortMode = (typeof sortModes)[number]; +const cityIds = new Set(cities.map(({ id }) => id)); +const activitySchema = z.enum(activities); +const sortModeSchema = z.enum(sortModes); +const cityIdSchema = z + .string() + .refine((id): id is CityId => cityIds.has(id as CityId)); +const favoriteIdsSchema = z.array(cityIdSchema); -type WeatherContentProps = { - city: City; +function Header({ + selectedCityId, + activity, + sortMode, + onCityChange, + onActivityChange, + onSortChange, +}: { + selectedCityId: CityId; activity: Activity; - weather: CityWeather | null; - isLoading: boolean; - error: Error | null; - onRetry: () => void; + sortMode: SortMode; + onCityChange: (cityId: CityId) => void; + onActivityChange: (activity: Activity) => void; + onSortChange: (sortMode: SortMode) => void; +}) { + return ( +
+

IT2810 · Gruppe 31

+

Turvær Norge

+

Finn været for din neste tur.

+
+ + + +
+
+ ); +} + +const weatherTypes: Record = { + 0: 'Klarvær', + 1: 'Hovedsakelig klart', + 2: 'Delvis skyet', + 3: 'Overskyet', + 45: 'Tåke', + 48: 'Underkjølt tåke', + 51: 'Lett yr', + 53: 'Yr', + 55: 'Kraftig yr', + 61: 'Lett regn', + 63: 'Regn', + 65: 'Kraftig regn', + 71: 'Lett snø', + 73: 'Snø', + 75: 'Kraftig snø', + 80: 'Lette regnbyger', + 81: 'Regnbyger', + 82: 'Kraftige regnbyger', + 95: 'Tordenvær', + 96: 'Tordenvær med hagl', + 99: 'Tordenvær med kraftig hagl', }; -function WeatherContent({ +function scoreWeather(weather: CityWeather | null, activity: Activity) { + if (!weather) return Number.NEGATIVE_INFINITY; + const { + temperature_2m: temperature, + precipitation, + wind_speed_10m: wind, + } = weather.current; + const profile = { + Gåtur: { + idealTemperature: 14, + temperatureWeight: 2, + rainWeight: 16, + windWeight: 3, + }, + Løpetur: { + idealTemperature: 11, + temperatureWeight: 3, + rainWeight: 24, + windWeight: 4, + }, + Skitur: { + idealTemperature: -3, + temperatureWeight: 4, + rainWeight: 10, + windWeight: 2, + }, + }[activity]; + const snowCode = + weather.current.weather_code >= 71 && weather.current.weather_code <= 77; + const snowBonus = activity === 'Skitur' && snowCode ? 35 : 0; + return ( + 100 - + Math.abs(temperature - profile.idealTemperature) * + profile.temperatureWeight - + precipitation * profile.rainWeight - + wind * profile.windWeight + + snowBonus + ); +} + +function MainWeather({ city, - activity, weather, + activity, isLoading, error, onRetry, -}: WeatherContentProps) { + onClose, +}: { + city: City; + weather: CityWeather | null; + activity: Activity; + isLoading: boolean; + error: Error | null; + onRetry: () => void; + onClose: () => void; +}) { return ( -
-

Været i {city.name}

-

Valgt aktivitet: {activity}

+
+
+
+

Valgt by

+

Været i {city.name}

+
+ +
+

+ Vurdert for {activity.toLowerCase()}. +

{isLoading &&

Henter værdata…

} {error && (
-

Kunne ikke hente værdata. Prøv igjen om litt.

+

Kunne ikke hente værdata akkurat nå.

@@ -39,127 +199,174 @@ function WeatherContent({ )} {weather && ( <> -
+
-
Temperatur
-
{weather.current.temperature_2m} °C
+

+ {weatherTypes[weather.current.weather_code] ?? 'Ukjent værtype'} +

+

{weather.current.temperature_2m}°

+

+ Oppdatert{' '} + +

+
+
-
Nedbør siste {weather.current.interval / 60} min
+
Nedbør
{weather.current.precipitation} mm
Vind
{weather.current.wind_speed_10m} m/s
+
+
Intervall
+
{weather.current.interval / 60} min
+
-

- Oppdatert{' '} - {' '} - (norsk tid). -

)}
); } -function Header({ - city, - activity, - onCityChange, - onActivityChange, -}: { - city: City; - activity: Activity; - onCityChange: (city: City) => void; - onActivityChange: (activity: Activity) => void; -}) { - return ( -
-

IT2810 · Gruppe 31

-

Turvær Norge

-

Finn været for din neste tur.

- - -
- ); -} - export function App() { - const [selectedCity, setSelectedCity] = useState(exampleCity); - const [selectedActivity, setSelectedActivity] = useState('Gåtur'); + const [selectedCityId, setSelectedCityId] = useStoredState( + 'sessionStorage', + 't31-selected-city', + cityIdSchema, + exampleCity.id, + ); + const [activity, setActivity] = useStoredState( + 'sessionStorage', + 't31-activity', + activitySchema, + 'Gåtur', + ); + const [sortMode, setSortMode] = useStoredState( + 'sessionStorage', + 't31-sort-mode', + sortModeSchema, + 'default', + ); + const [favoriteIds, setFavoriteIds] = useStoredState( + 'localStorage', + 't31-favorite-cities', + favoriteIdsSchema, + [], + ); + const [detailCityId, setDetailCityId] = useState(null); const weatherQueries = useQueries({ queries: cities.map((city) => weatherQuery(city)), }); - const selectedCityIndex = cities.findIndex( - ({ id }) => id === selectedCity.id, + const selectedCity = + cities.find(({ id }) => id === selectedCityId) ?? exampleCity; + const items = useMemo( + () => + cities.map((city, index) => { + const query = weatherQueries[index]; + return { + city, + weather: query?.data ?? null, + isLoading: query?.isPending ?? true, + hasError: query?.isError ?? false, + isFavorite: favoriteIds.includes(city.id), + score: scoreWeather(query?.data ?? null, activity), + rank: null, + }; + }), + [activity, favoriteIds, weatherQueries], ); - const weatherQueryResult = weatherQueries[selectedCityIndex]; - const weatherListItems: WeatherListItem[] = cities.map((city, index) => { - const query = weatherQueries[index]; - if (!query) { - return { city, weather: null, isLoading: true, hasError: false }; - } - return { - city, - weather: query.data ?? null, - isLoading: query.isPending, - hasError: query.isError, - }; - }); - - const weatherData = weatherQueryResult?.data ?? null; + const sortedItems = useMemo( + () => + [...items].sort((a, b) => { + if (sortMode === 'best') return b.score - a.score; + if (sortMode === 'temperature') + return ( + (b.weather?.current.temperature_2m ?? -Infinity) - + (a.weather?.current.temperature_2m ?? -Infinity) + ); + if (sortMode === 'rain') + return ( + (a.weather?.current.precipitation ?? Infinity) - + (b.weather?.current.precipitation ?? Infinity) + ); + if (sortMode === 'wind') + return ( + (a.weather?.current.wind_speed_10m ?? Infinity) - + (b.weather?.current.wind_speed_10m ?? Infinity) + ); + return 0; + }), + [items, sortMode], + ); + const rankedItems = sortedItems.map((item, index) => ({ + ...item, + rank: sortMode === 'best' && Number.isFinite(item.score) ? index + 1 : null, + })); + const detailCity = cities.find(({ id }) => id === detailCityId) ?? null; + const detailItem = rankedItems.find(({ city }) => city.id === detailCityId); + const toggleFavorite = (city: City) => + setFavoriteIds( + favoriteIds.includes(city.id) + ? favoriteIds.filter((id) => id !== city.id) + : [...favoriteIds, city.id], + ); return (
- void weatherQueryResult?.refetch()} + selectedCityId={selectedCity.id} + activity={activity} + sortMode={sortMode} + onCityChange={setSelectedCityId} + onActivityChange={setActivity} + onSortChange={setSortMode} /> setSelectedCityId(city.id)} + onOpenDetails={(city) => { + setSelectedCityId(city.id); + setDetailCityId(city.id); + }} + onToggleFavorite={toggleFavorite} + expandedCityId={detailCityId} + details={ + detailCity ? ( +
+
+ ) : null + } />
Værdata fra Open-Meteo,{' '} diff --git a/web/src/WeatherCard.tsx b/web/src/WeatherCard.tsx index cae4edb..4bda909 100644 --- a/web/src/WeatherCard.tsx +++ b/web/src/WeatherCard.tsx @@ -8,6 +8,10 @@ export type WeatherCardProps = { hasError: boolean; isSelected: boolean; onSelect: (city: City) => void; + onOpenDetails: (city: City) => void; + isFavorite: boolean; + onToggleFavorite: (city: City) => void; + rank: number | null; }; const weatherTypes: Record = { @@ -41,6 +45,10 @@ export function WeatherCard({ hasError, isSelected, onSelect, + onOpenDetails, + isFavorite, + onToggleFavorite, + rank, }: WeatherCardProps) { const weatherType = weather ? (weatherTypes[weather.current.weather_code] ?? 'Ukjent værtype') @@ -48,11 +56,15 @@ export function WeatherCard({ return (

{city.name}

+ {rank !== null && ( +

#{rank} for valgt aktivitet

+ )} + + {isLoading &&

Henter værdata…

} {hasError &&

Kunne ikke hente værdata.

} {weather && ( diff --git a/web/src/WeatherExample.test.tsx b/web/src/WeatherExample.test.tsx index 0286749..d0f73e3 100644 --- a/web/src/WeatherExample.test.tsx +++ b/web/src/WeatherExample.test.tsx @@ -6,7 +6,7 @@ import { http, HttpResponse } from 'msw'; import { App } from './App'; import { WeatherExample } from './WeatherExample'; import { WEATHER_URL, fetchWeather } from './features/weather/weather'; -import { exampleCity } from './features/weather/cities'; +import { cities, exampleCity } from './features/weather/cities'; import { server } from './test/server'; import { weatherResponse } from './test/fixtures'; function renderExample() { @@ -35,7 +35,7 @@ it('renders validated weather and a stable snapshot', async () => { }); it('stores selected city and activity in App state', async () => { renderApp(); - await screen.findByRole('heading', { name: 'Været i Trondheim' }); + await screen.findByRole('heading', { name: 'Vær i norske byer' }); await userEvent.selectOptions( screen.getByRole('combobox', { name: 'By' }), @@ -46,13 +46,133 @@ it('stores selected city and activity in App state', async () => { 'Løpetur', ); + expect(screen.getByRole('button', { name: 'Vær i Oslo' })).toHaveAttribute( + 'aria-pressed', + 'true', + ); + await userEvent.click( + screen.getByRole('button', { name: 'Se detaljer for Oslo' }), + ); expect(screen.getByRole('heading', { name: 'Været i Oslo' })).toBeVisible(); - expect(screen.getByText('Valgt aktivitet: Løpetur')).toBeVisible(); + expect(screen.getByText('løpetur')).toBeVisible(); + + await userEvent.click(screen.getByRole('button', { name: 'Vær i Bergen' })); + expect(screen.getByRole('button', { name: 'Vær i Bergen' })).toHaveAttribute( + 'aria-pressed', + 'true', + ); +}); +it('navigates the main resource and persists sorting and favorites', async () => { + const { unmount } = renderApp(); + await screen.findByRole('heading', { name: 'Vær i norske byer' }); + + await userEvent.click( + screen.getByRole('button', { name: 'Se detaljer for Trondheim' }), + ); + expect( + screen.getByRole('heading', { name: 'Været i Trondheim' }), + ).toBeVisible(); + expect( + screen.queryByRole('button', { name: 'Neste by' }), + ).not.toBeInTheDocument(); - const bergenCard = screen.getByRole('button', { name: 'Vær i Bergen' }); - bergenCard.focus(); - await userEvent.keyboard('{Enter}'); - expect(screen.getByRole('heading', { name: 'Været i Bergen' })).toBeVisible(); + await userEvent.selectOptions( + screen.getByRole('combobox', { name: 'Sorter etter' }), + 'best', + ); + await userEvent.click(screen.getByRole('button', { name: /Legg Oslo til/ })); + expect( + screen.getByRole('button', { name: /Fjern Oslo fra/ }), + ).toHaveAttribute('aria-pressed', 'true'); + + unmount(); + renderApp(); + expect(screen.getByRole('combobox', { name: 'Sorter etter' })).toHaveValue( + 'best', + ); + expect(screen.getByRole('button', { name: /Fjern Oslo fra/ })).toBeVisible(); +}); +it('changes the top activity ranking when the activity changes', async () => { + server.use( + http.get(WEATHER_URL, ({ request }) => { + const latitude = new URL(request.url).searchParams.get('latitude'); + const city = cities.find( + ({ latitude: cityLatitude }) => String(cityLatitude) === latitude, + ); + const profiles = { + oslo: { + temperature_2m: 16, + precipitation: 0, + wind_speed_10m: 2, + weather_code: 0, + }, + bergen: { + temperature_2m: 8, + precipitation: 4, + wind_speed_10m: 6, + weather_code: 61, + }, + trondheim: { + temperature_2m: -2, + precipitation: 0, + wind_speed_10m: 3, + weather_code: 73, + }, + stavanger: { + temperature_2m: 12, + precipitation: 0, + wind_speed_10m: 2, + weather_code: 0, + }, + tromso: { + temperature_2m: -5, + precipitation: 1, + wind_speed_10m: 4, + weather_code: 71, + }, + bodo: { + temperature_2m: 6, + precipitation: 2, + wind_speed_10m: 5, + weather_code: 3, + }, + kristiansand: { + temperature_2m: 16, + precipitation: 0, + wind_speed_10m: 3, + weather_code: 0, + }, + alesund: { + temperature_2m: 10, + precipitation: 1, + wind_speed_10m: 4, + weather_code: 63, + }, + } as const; + const profile = profiles[city?.id ?? 'oslo']; + return HttpResponse.json({ + ...weatherResponse, + current: { ...weatherResponse.current, ...profile }, + }); + }), + ); + renderApp(); + await screen.findByRole('heading', { name: 'Vær i norske byer' }); + await userEvent.selectOptions( + screen.getByRole('combobox', { name: 'Sorter etter' }), + 'best', + ); + expect( + screen.getByRole('article', { name: 'Værkort for Oslo' }), + ).toHaveTextContent('#1 for valgt aktivitet'); + + await userEvent.selectOptions( + screen.getByRole('combobox', { name: 'Aktivitet' }), + 'Skitur', + ); + expect( + screen.getByRole('article', { name: 'Værkort for Trondheim' }), + ).toHaveTextContent('#1 for valgt aktivitet'); }); it('shows errors and retries on user interaction', async () => { server.use( diff --git a/web/src/WeatherList.tsx b/web/src/WeatherList.tsx index 3904eee..09e98e9 100644 --- a/web/src/WeatherList.tsx +++ b/web/src/WeatherList.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react'; import type { City } from './features/weather/cities'; import type { CityWeather } from './features/weather/weather'; import { WeatherCard } from './WeatherCard'; @@ -7,33 +8,57 @@ export type WeatherListItem = { weather: CityWeather | null; isLoading: boolean; hasError: boolean; + isFavorite: boolean; + score: number; + rank: number | null; }; export type WeatherListProps = { items: WeatherListItem[]; selectedCity: City; onCitySelect: (city: City) => void; + onOpenDetails: (city: City) => void; + onToggleFavorite: (city: City) => void; + expandedCityId: City['id'] | null; + details: ReactNode; }; export function WeatherList({ items, selectedCity, onCitySelect, + onOpenDetails, + onToggleFavorite, + expandedCityId, + details, }: WeatherListProps) { return (

Vær i norske byer

{items.map((item) => ( - + > + + {item.city.id === expandedCityId && details} +
))}
diff --git a/web/src/styles.css b/web/src/styles.css index af2bf8f..6a50cb2 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -11,27 +11,148 @@ body { margin: 0; } main { - width: min(680px, 100% - 2rem); - margin: 3rem auto; + width: min(1120px, 100% - 2rem); + margin: 2rem auto 4rem; } h1 { font-size: clamp(2rem, 6vw, 3rem); line-height: 1.1; } -header { +.app-header { + display: flex; + justify-content: space-between; + gap: 2rem; + align-items: end; margin-bottom: 2rem; } +.eyebrow { + margin: 0; + color: #9a491c; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} +.controls { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} +label { + display: grid; + gap: 0.25rem; + font-size: 0.85rem; + font-weight: 600; +} +select { + min-height: 44px; + padding: 0.5rem 2rem 0.5rem 0.65rem; + border: 1px solid #bacac1; + border-radius: 0.25rem; + background: #fff; + color: inherit; + font: inherit; +} section { border: 1px solid #bacac1; background: #fff; padding: clamp(1rem, 4vw, 2rem); border-radius: 0.5rem; } +.main-weather { + margin-bottom: 1.5rem; +} +.section-heading { + display: flex; + align-items: start; + justify-content: space-between; + gap: 1rem; +} +.section-heading h2 { + margin: 0.25rem 0 0; +} +.resource-navigation { + display: flex; + gap: 0.5rem; +} +.main-weather-summary { + display: flex; + justify-content: space-between; + align-items: end; + border-block: 1px solid #d5e2dc; + margin: 1.5rem 0; + padding: 1rem 0; +} +.weather-type, +.update-time { + margin: 0; +} +.weather-type { + font-weight: 700; +} +.temperature { + margin: 0.25rem 0 0; + font-size: clamp(3rem, 8vw, 5rem); + line-height: 1; + font-weight: 700; +} +.weather-details { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; +} +.weather-details dd { + font-size: 1.3rem; +} .weather-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: 1rem; } +.weather-card-slot { + min-width: 0; +} +.weather-card-slot-expanded { + position: relative; + z-index: 10; +} +.expanded-weather { + width: 100%; + margin: 0; +} +.details-overlay { + position: fixed; + inset: 0; + z-index: 100; + display: grid; + place-items: center; + padding: 1rem; + background: rgb(23 61 53 / 35%); +} +.details-backdrop { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + min-height: 0; + padding: 0; + border: 0; + background: transparent; + cursor: default; +} +.details-dialog { + position: relative; + z-index: 1; + width: min(42rem, 100%); + max-height: calc(100vh - 2rem); + overflow: auto; + border-radius: 0.5rem; + box-shadow: 0 1rem 2.5rem rgb(23 61 53 / 25%); +} +.expanded-weather .resource-navigation { + flex-wrap: wrap; + justify-content: flex-end; +} .weather-card { border: 1px solid #bacac1; border-radius: 0.5rem; @@ -39,6 +160,9 @@ section { background: #fff; cursor: pointer; } +.weather-card button + button { + margin-left: 0.5rem; +} .weather-card:hover, .weather-card:focus-visible, .weather-card-selected { @@ -48,6 +172,12 @@ section { .weather-card h3 { margin-top: 0; } +.weather-rank { + margin: -0.5rem 0 0.75rem; + color: #9a491c; + font-size: 0.8rem; + font-weight: 700; +} .weather-card dl { gap: 0.75rem 1rem; } @@ -77,6 +207,9 @@ button { border-radius: 0.25rem; cursor: pointer; } +button[aria-pressed='true'] { + background: #9a491c; +} a { color: inherit; text-underline-offset: 0.2em; @@ -89,3 +222,25 @@ footer { margin-top: 2rem; font-size: 0.85rem; } +@media (max-width: 760px) { + .app-header, + .section-heading, + .main-weather-summary { + align-items: stretch; + flex-direction: column; + } + .controls, + .controls label, + .controls select { + width: 100%; + } + .weather-details { + grid-template-columns: 1fr; + } + .resource-navigation button { + flex: 1; + } + .details-dialog { + width: 100%; + } +} From e4f40644070951cbe970547f406fb6488644057a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20=C3=98rjavik?= Date: Mon, 14 Sep 2026 11:26:48 +0200 Subject: [PATCH 10/21] docs(weather): add minor tweaks to architecture and weather cards docs --- docs/weather-app-architecture.md | 2 -- docs/weather-cards-and-ranking.md | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/weather-app-architecture.md b/docs/weather-app-architecture.md index 4ee02b8..d6ac665 100644 --- a/docs/weather-app-architecture.md +++ b/docs/weather-app-architecture.md @@ -15,8 +15,6 @@ App └── MainWeather (centered overlay while that card is expanded) ``` -`WeatherExample.tsx` remains a small API example used by the existing provider tests. The production page uses all configured cities and TanStack Query. - ## State and persistence - The selected city is stored in `sessionStorage` under `t31-selected-city`. diff --git a/docs/weather-cards-and-ranking.md b/docs/weather-cards-and-ranking.md index b7b75c1..bf2f931 100644 --- a/docs/weather-cards-and-ranking.md +++ b/docs/weather-cards-and-ranking.md @@ -2,7 +2,7 @@ ## New page flow -The weather cards are now the default main view. The page opens directly on the city collection so users can compare the available cities at a glance. +The weather cards are the default main view. The page opens directly on the city collection so users can compare the available cities at a glance. Each card provides: From 565282f7ab6db661399b3e3711b9e237a76af024 Mon Sep 17 00:00:00 2001 From: Hasan Mohammad Fawzi Date: Tue, 15 Sep 2026 01:52:07 +0200 Subject: [PATCH 11/21] docs: choose Open-Meteo weather API --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index ebeaa0a..47a8374 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,37 @@ Applikasjonen skal: Værdata hentes direkte når applikasjonen brukes og mellomlagres ikke. +## Værdata + +Prosjektet bruker Open-Meteo Forecast API. + +API-et kan brukes direkte fra nettleseren uten API-nøkkel. Byens +breddegrad og lengdegrad sendes til `/v1/forecast`. Applikasjonen ber om +temperatur, følt temperatur, luftfuktighet, nedbør, værkode, vindstyrke +og informasjon om dag eller natt. + +### Valgte værdata + +Vi velger følgende data: + +| Felt | Bruk | +| ---------------------- | ---------------------- | +| `temperature_2m` | Temperatur nå | +| `apparent_temperature` | Føles som | +| `weather_code` | Værtype og ikon | +| `precipitation` | Nedbør | +| `wind_speed_10m` | Vindstyrke | +| `relative_humidity_2m` | Luftfuktighet | +| `is_day` | Dag/natt | +| `time` | Tidspunkt for målingen | + + +Eksempel: + +```bash +https://api.open-meteo.com/v1/forecast?latitude=63.4305&longitude=10.3951¤t=temperature_2m,apparent_temperature,relative_humidity_2m,precipitation,weather_code,wind_speed_10m,is_day&wind_speed_unit=ms&timezone=auto +``` + ## Teknologi React, TypeScript, Vite, TanStack Query, `fetch`, plain CSS, ESLint, Prettier og Vitest. Eksemplet bruker [Open-Meteo](https://open-meteo.com/en/docs). From 1a6cd2fe1b868f7ff59668e5ac024c76c0312bcd Mon Sep 17 00:00:00 2001 From: Oliver Dragland Date: Tue, 15 Sep 2026 11:31:53 +0200 Subject: [PATCH 12/21] fix(e2e): scope weather detail assertion --- e2e/weather.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/e2e/weather.spec.ts b/e2e/weather.spec.ts index 94bad8d..765e485 100644 --- a/e2e/weather.spec.ts +++ b/e2e/weather.spec.ts @@ -18,7 +18,11 @@ test('weather example loads accessibly without horizontal overflow', async ({ await expect( page.getByRole('heading', { name: 'Været i Trondheim' }), ).toBeVisible(); - await expect(page.getByText('14 °C')).toBeVisible(); + await expect( + page + .locator('section[aria-labelledby="selected-weather-title"]') + .getByText('14°'), + ).toBeVisible(); await expect( page.getByRole('heading', { name: 'Vær i norske byer' }), ).toBeVisible(); From 2344f5d903593c2285a00ee2836e1e0c47d1333a Mon Sep 17 00:00:00 2001 From: Hasan Mohammad Fawzi Date: Tue, 15 Sep 2026 13:28:44 +0200 Subject: [PATCH 13/21] feat: implement Open-Meteo weather service (#25) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: implement Open-Meteo weather service * Update README.md Co-authored-by: Oliver Ørjavik * Update web/src/services/weatherService.ts Co-authored-by: Oliver Ørjavik --------- Co-authored-by: Hasan Mohammad Fawzi Co-authored-by: Oliver Ørjavik --- README.md | 63 +++++++++++++++++++++++++++++- web/src/services/weatherService.ts | 60 ++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 web/src/services/weatherService.ts diff --git a/README.md b/README.md index 0efbaa2..46c78cf 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,68 @@ Applikasjonen skal: - vise tydelig lasting og feilmelding ved API-feil - fungere på både mobil og desktop -Værdata hentes direkte når applikasjonen brukes og mellomlagres ikke. +Værdata hentes direkte når applikasjonen brukes og mellomlagres ikke +## Weather service + +`weatherService` har ansvar for å hente værdata fra Open-Meteo og konvertere API-responsen til prosjektets egen `WeatherData`-type. + +Dette gjør at React-komponentene ikke trenger å kjenne til: + +* URL-en til Open-Meteo +* feltnavnene i API-responsen +* hvordan API-kallet bygges +* hvordan HTTP-feil håndteres + +Komponentene arbeider bare med prosjektets egne TypeScript-typer. + +### Hente værdata + +```ts +const weather = await getWeather(city); +``` + +Her skjer følgende: + +1. `city` inneholder navn, breddegrad og lengdegrad. +2. `getWeather(city)` sender et `fetch`-kall til Open-Meteo. +3. `await` venter til API-kallet er ferdig. +4. Relevante felter fra API-responsen mappes til prosjektets egen WeatherData-type. +5. Resultatet lagres i variabelen `weather`. + +Variabelen kan deretter brukes slik: + +```ts +console.log(weather.temperature); +console.log(weather.windSpeed); +console.log(weather.precipitation); +``` + +### Eksempel med feilhåndtering + +```ts +try { + const weather = await getWeather(city); + console.log(weather); +} catch (error) { + console.error('Kunne ikke hente værdata:', error); +} +``` + +Hvis API-kallet mislykkes, kaster `getWeather` en feil som React-komponenten kan fange og vise som en forståelig feilmelding til brukeren. + +### Dataflyt + +```mermaid +flowchart LR + City["City"] --> Service["getWeather(city)"] + Service --> API["Open-Meteo"] + API --> Service + Service --> Data["WeatherData"] + Data --> Component["React-komponent"] +``` + +Denne oppdelingen gjør API-håndteringen samlet på ett sted og lar React-komponentene bruke en enkel og stabil `WeatherData`-modell. + ## Værdata diff --git a/web/src/services/weatherService.ts b/web/src/services/weatherService.ts new file mode 100644 index 0000000..63fbf94 --- /dev/null +++ b/web/src/services/weatherService.ts @@ -0,0 +1,60 @@ +import type { City } from '../types/city'; +import type { WeatherData } from '../types/weather'; + +interface OpenMeteoCurrent { + time: string; // Local time (ISO 8601) + temperature_2m: number; // °C + apparent_temperature: number; // °C + relative_humidity_2m: number; // % + precipitation: number; // mm + weather_code: number; // WMO weather code + wind_speed_10m: number; // m/s + is_day: number; // 0 = night, 1 = day +} + +interface OpenMeteoResponse { + current: OpenMeteoCurrent; +} + +const API_URL = 'https://api.open-meteo.com/v1/forecast'; + +export async function getWeather(city: City): Promise { + const parameters = new URLSearchParams({ + latitude: city.latitude.toString(), + longitude: city.longitude.toString(), + current: [ + 'temperature_2m', + 'apparent_temperature', + 'relative_humidity_2m', + 'precipitation', + 'weather_code', + 'wind_speed_10m', + 'is_day', + ].join(','), + wind_speed_unit: 'ms', + timezone: 'auto', + }); + + const response = await fetch(`${API_URL}?${parameters.toString()}`); + + if (!response.ok) { + throw new Error( + `Kunne ikke hente værdata for ${city.name}. HTTP ${response.status}`, + ); + } + + const apiData = (await response.json()) as OpenMeteoResponse; + const current = apiData.current; + + return { + cityId: city.id, + time: current.time, + temperature: current.temperature_2m, + apparentTemperature: current.apparent_temperature, + humidity: current.relative_humidity_2m, + precipitation: current.precipitation, + weatherCode: current.weather_code, + windSpeed: current.wind_speed_10m, + isDay: current.is_day === 1, + }; +} \ No newline at end of file From e77f2260b33aa20f9daa6a923325c0ace9a329ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20=C3=98rjavik?= Date: Tue, 15 Sep 2026 14:15:34 +0200 Subject: [PATCH 14/21] fix(weather): add direct retry action to error cards --- web/src/App.tsx | 3 +++ web/src/WeatherCard.tsx | 11 ++++++++++- web/src/WeatherList.tsx | 2 ++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index f6f1873..e8510e5 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -273,6 +273,9 @@ export function App() { weather: query?.data ?? null, isLoading: query?.isPending ?? true, hasError: query?.isError ?? false, + onRetry: () => { + void query?.refetch(); + }, isFavorite: favoriteIds.includes(city.id), score: scoreWeather(query?.data ?? null, activity), rank: null, diff --git a/web/src/WeatherCard.tsx b/web/src/WeatherCard.tsx index 4bda909..71a375e 100644 --- a/web/src/WeatherCard.tsx +++ b/web/src/WeatherCard.tsx @@ -6,6 +6,7 @@ export type WeatherCardProps = { weather: CityWeather | null; isLoading: boolean; hasError: boolean; + onRetry: () => void; isSelected: boolean; onSelect: (city: City) => void; onOpenDetails: (city: City) => void; @@ -43,6 +44,7 @@ export function WeatherCard({ weather, isLoading, hasError, + onRetry, isSelected, onSelect, onOpenDetails, @@ -93,7 +95,14 @@ export function WeatherCard({ {isFavorite ? '★ Favoritt' : '☆ Favoritt'} {isLoading &&

Henter værdata…

} - {hasError &&

Kunne ikke hente værdata.

} + {hasError && ( +
+

Kunne ikke hente værdata.

+ +
+ )} {weather && (
diff --git a/web/src/WeatherList.tsx b/web/src/WeatherList.tsx index 09e98e9..2676e67 100644 --- a/web/src/WeatherList.tsx +++ b/web/src/WeatherList.tsx @@ -8,6 +8,7 @@ export type WeatherListItem = { weather: CityWeather | null; isLoading: boolean; hasError: boolean; + onRetry: () => void; isFavorite: boolean; score: number; rank: number | null; @@ -50,6 +51,7 @@ export function WeatherList({ weather={item.weather} isLoading={item.isLoading} hasError={item.hasError} + onRetry={item.onRetry} isSelected={item.city.id === selectedCity.id} onSelect={onCitySelect} onOpenDetails={onOpenDetails} From 72f393fac343ee6f9363e6b5fbc69dca00cac0a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20=C3=98rjavik?= Date: Tue, 15 Sep 2026 14:17:06 +0200 Subject: [PATCH 15/21] fix(ui): apply app-header styling class --- web/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index e8510e5..a537761 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -39,7 +39,7 @@ function Header({ onSortChange: (sortMode: SortMode) => void; }) { return ( -
+

IT2810 · Gruppe 31

Turvær Norge

Finn været for din neste tur.

From 129dc2685e7963300cec4c3bfae1f3d1854da213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20=C3=98rjavik?= Date: Tue, 15 Sep 2026 14:19:47 +0200 Subject: [PATCH 16/21] fix(accessibility): trap focus in weather details dialog --- web/src/App.tsx | 122 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 95 insertions(+), 27 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index a537761..effcc9f 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,4 +1,5 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import type { ReactNode } from 'react'; import { useQueries } from '@tanstack/react-query'; import { z } from 'zod'; import { @@ -154,6 +155,81 @@ function scoreWeather(weather: CityWeather | null, activity: Activity) { ); } +function DetailsOverlay({ + children, + onClose, +}: { + children: ReactNode; + onClose: () => void; +}) { + const overlayRef = useRef(null); + + useEffect(() => { + const overlay = overlayRef.current; + if (!overlay) return; + const previouslyFocused = document.activeElement as HTMLElement | null; + const focusableSelector = + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; + const getFocusable = () => + Array.from( + overlay.querySelectorAll(focusableSelector), + ).filter((element) => !element.hasAttribute('disabled')); + + (getFocusable()[0] ?? overlay).focus(); + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + event.preventDefault(); + onClose(); + return; + } + if (event.key !== 'Tab') return; + + const focusable = getFocusable(); + if (focusable.length === 0) { + event.preventDefault(); + overlay.focus(); + return; + } + + const first = focusable[0]!; + const last = focusable[focusable.length - 1]!; + if (event.shiftKey && document.activeElement === first) { + event.preventDefault(); + last.focus(); + } else if (!event.shiftKey && document.activeElement === last) { + event.preventDefault(); + first.focus(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('keydown', handleKeyDown); + previouslyFocused?.focus(); + }; + }, [onClose]); + + return ( +
+
+ ); +} + function MainWeather({ city, weather, @@ -341,33 +417,25 @@ export function App() { expandedCityId={detailCityId} details={ detailCity ? ( -
-
+ ) : null } /> From 82952e695140644a35d00fca333f73dffb7550ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20=C3=98rjavik?= Date: Tue, 15 Sep 2026 14:22:59 +0200 Subject: [PATCH 17/21] feat(weather): add empty state for weather list --- web/src/WeatherList.test.tsx | 25 +++++++++++++++++ web/src/WeatherList.tsx | 54 +++++++++++++++++++----------------- 2 files changed, 54 insertions(+), 25 deletions(-) create mode 100644 web/src/WeatherList.test.tsx diff --git a/web/src/WeatherList.test.tsx b/web/src/WeatherList.test.tsx new file mode 100644 index 0000000..2c58173 --- /dev/null +++ b/web/src/WeatherList.test.tsx @@ -0,0 +1,25 @@ +import { render, screen } from '@testing-library/react'; +import { vi, expect, it } from 'vitest'; +import { WeatherList } from './WeatherList'; +import { exampleCity } from './features/weather/cities'; + +it('shows feedback when there are no weather cards to display', () => { + render( + , + ); + + expect( + screen.getByRole('heading', { name: 'Vær i norske byer' }), + ).toBeVisible(); + expect(screen.getByRole('status')).toHaveTextContent( + 'Ingen værkort å vise.', + ); +}); diff --git a/web/src/WeatherList.tsx b/web/src/WeatherList.tsx index 2676e67..865b301 100644 --- a/web/src/WeatherList.tsx +++ b/web/src/WeatherList.tsx @@ -37,31 +37,35 @@ export function WeatherList({

Vær i norske byer

- {items.map((item) => ( -
- - {item.city.id === expandedCityId && details} -
- ))} + {items.length === 0 ? ( +

Ingen værkort å vise.

+ ) : ( + items.map((item) => ( +
+ + {item.city.id === expandedCityId && details} +
+ )) + )}
); From 8832bee284850c370986795c15f0ceb7947e6ec9 Mon Sep 17 00:00:00 2001 From: Hasan Mohammad Fawzi Date: Wed, 16 Sep 2026 00:41:42 +0200 Subject: [PATCH 18/21] feat: add city weather and activity models (#24) * feat: add city weather and activity models * feat: Add correct code for acitivity.ts --------- Co-authored-by: Hasan Mohammad Fawzi --- web/src/data/activities.ts | 24 ++++++++++++++++++++++++ web/src/data/cities.ts | 34 ++++++++++++++++++++++++++++++++++ web/src/type/activity.ts | 11 +++++++++++ web/src/type/city.ts | 6 ++++++ web/src/type/weather.ts | 11 +++++++++++ 5 files changed, 86 insertions(+) create mode 100644 web/src/data/activities.ts create mode 100644 web/src/data/cities.ts create mode 100644 web/src/type/activity.ts create mode 100644 web/src/type/city.ts create mode 100644 web/src/type/weather.ts diff --git a/web/src/data/activities.ts b/web/src/data/activities.ts new file mode 100644 index 0000000..a05d767 --- /dev/null +++ b/web/src/data/activities.ts @@ -0,0 +1,24 @@ +import type { Activity } from '../types/activity'; + +export const activities: Activity[] = [ + { + id: 'walking', + name: 'Gåtur', + description: 'En rolig tur utendørs.', + }, + { + id: 'cycling', + name: 'Sykkeltur', + description: 'En tur på sykkel.', + }, + { + id: 'running', + name: 'Løping', + description: 'Løping eller jogging utendørs.', + }, + { + id: 'skiing', + name: 'Skitur', + description: 'Langrenn eller annen aktivitet på ski.', + }, +]; \ No newline at end of file diff --git a/web/src/data/cities.ts b/web/src/data/cities.ts new file mode 100644 index 0000000..df23203 --- /dev/null +++ b/web/src/data/cities.ts @@ -0,0 +1,34 @@ +import type { City } from '../types/city'; + +export const cities: City[] = [ + { + id: 'oslo', + name: 'Oslo', + latitude: 59.9139, + longitude: 10.7522, + }, + { + id: 'bergen', + name: 'Bergen', + latitude: 60.3913, + longitude: 5.3221, + }, + { + id: 'trondheim', + name: 'Trondheim', + latitude: 63.4305, + longitude: 10.3951, + }, + { + id: 'stavanger', + name: 'Stavanger', + latitude: 58.97, + longitude: 5.7331, + }, + { + id: 'tromso', + name: 'Tromsø', + latitude: 69.6492, + longitude: 18.9553, + }, +]; \ No newline at end of file diff --git a/web/src/type/activity.ts b/web/src/type/activity.ts new file mode 100644 index 0000000..65280d1 --- /dev/null +++ b/web/src/type/activity.ts @@ -0,0 +1,11 @@ +export type ActivityId = + | 'walking' + | 'cycling' + | 'running' + | 'skiing'; + +export interface Activity { + id: ActivityId; + name: string; + description: string; +} \ No newline at end of file diff --git a/web/src/type/city.ts b/web/src/type/city.ts new file mode 100644 index 0000000..fca4ad6 --- /dev/null +++ b/web/src/type/city.ts @@ -0,0 +1,6 @@ +export interface City { + id: string; + name: string; + latitude: number; + longitude: number; +} \ No newline at end of file diff --git a/web/src/type/weather.ts b/web/src/type/weather.ts new file mode 100644 index 0000000..eb38e1e --- /dev/null +++ b/web/src/type/weather.ts @@ -0,0 +1,11 @@ +export interface WeatherData { + cityId: string; + time: string; + temperature: number; + apparentTemperature: number; + humidity: number; + precipitation: number; + weatherCode: number; + windSpeed: number; + isDay: boolean; +} \ No newline at end of file From 48ba68f907fe51d6d3ce3f013e65cba5d8add80c Mon Sep 17 00:00:00 2001 From: Hasan Mohammad Fawzi Date: Thu, 17 Sep 2026 01:04:30 +0200 Subject: [PATCH 19/21] feat(weather): integrate shared city and activity models --- web/src/App.tsx | 83 +++++++++++++++++++++-------- web/src/WeatherExample.test.tsx | 27 +--------- web/src/data/cities.ts | 30 +++++++++-- web/src/features/weather/cities.ts | 21 ++------ web/src/type/city.ts | 6 --- web/src/{type => types}/activity.ts | 0 web/src/types/city.ts | 16 ++++++ web/src/{type => types}/weather.ts | 0 8 files changed, 109 insertions(+), 74 deletions(-) delete mode 100644 web/src/type/city.ts rename web/src/{type => types}/activity.ts (100%) create mode 100644 web/src/types/city.ts rename web/src/{type => types}/weather.ts (100%) diff --git a/web/src/App.tsx b/web/src/App.tsx index effcc9f..1cd4222 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -12,12 +12,19 @@ import { weatherQuery, type CityWeather } from './features/weather/weather'; import { useStoredState } from './shared/storage'; import { WeatherList, type WeatherListItem } from './WeatherList'; -const activities = ['Gåtur', 'Løpetur', 'Skitur'] as const; -type Activity = (typeof activities)[number]; +import { activities } from './data/activities'; +import type { ActivityId } from './types/activity'; + + const sortModes = ['default', 'best', 'temperature', 'rain', 'wind'] as const; type SortMode = (typeof sortModes)[number]; const cityIds = new Set(cities.map(({ id }) => id)); -const activitySchema = z.enum(activities); +const activitySchema = z.enum([ + 'walking', + 'cycling', + 'running', + 'skiing', +]); const sortModeSchema = z.enum(sortModes); const cityIdSchema = z .string() @@ -33,10 +40,10 @@ function Header({ onSortChange, }: { selectedCityId: CityId; - activity: Activity; + activity: ActivityId; sortMode: SortMode; onCityChange: (cityId: CityId) => void; - onActivityChange: (activity: Activity) => void; +onActivityChange: (activity: ActivityId) => void; onSortChange: (sortMode: SortMode) => void; }) { return ( @@ -63,12 +70,12 @@ function Header({ @@ -115,36 +122,62 @@ const weatherTypes: Record = { 99: 'Tordenvær med kraftig hagl', }; -function scoreWeather(weather: CityWeather | null, activity: Activity) { +function scoreWeather( + weather: CityWeather | null, + activity: ActivityId, +) { if (!weather) return Number.NEGATIVE_INFINITY; + const { temperature_2m: temperature, precipitation, wind_speed_10m: wind, } = weather.current; - const profile = { - Gåtur: { + + const profiles: Record< + ActivityId, + { + idealTemperature: number; + temperatureWeight: number; + rainWeight: number; + windWeight: number; + } + > = { + walking: { idealTemperature: 14, temperatureWeight: 2, rainWeight: 16, windWeight: 3, }, - Løpetur: { + cycling: { + idealTemperature: 16, + temperatureWeight: 2, + rainWeight: 20, + windWeight: 5, + }, + running: { idealTemperature: 11, temperatureWeight: 3, rainWeight: 24, windWeight: 4, }, - Skitur: { + skiing: { idealTemperature: -3, temperatureWeight: 4, rainWeight: 10, windWeight: 2, }, - }[activity]; + }; + + const profile = profiles[activity]; + const snowCode = - weather.current.weather_code >= 71 && weather.current.weather_code <= 77; - const snowBonus = activity === 'Skitur' && snowCode ? 35 : 0; + weather.current.weather_code >= 71 && + weather.current.weather_code <= 77; + + const snowBonus = + activity === 'skiing' && snowCode ? 35 : 0; + return ( 100 - Math.abs(temperature - profile.idealTemperature) * @@ -155,6 +188,13 @@ function scoreWeather(weather: CityWeather | null, activity: Activity) { ); } +function getActivityName(activityId: ActivityId) { + return ( + activities.find((activity) => activity.id === activityId)?.name ?? + activityId + ); +} + function DetailsOverlay({ children, onClose, @@ -241,7 +281,7 @@ function MainWeather({ }: { city: City; weather: CityWeather | null; - activity: Activity; + activity: ActivityId; isLoading: boolean; error: Error | null; onRetry: () => void; @@ -262,7 +302,8 @@ function MainWeather({

- Vurdert for {activity.toLowerCase()}. + Vurdert for{' '} + {getActivityName(activity).toLowerCase()}.

{isLoading &&

Henter værdata…

} {error && ( @@ -316,11 +357,11 @@ export function App() { cityIdSchema, exampleCity.id, ); - const [activity, setActivity] = useStoredState( + const [activity, setActivity] = useStoredState( 'sessionStorage', 't31-activity', activitySchema, - 'Gåtur', + 'walking', ); const [sortMode, setSortMode] = useStoredState( 'sessionStorage', diff --git a/web/src/WeatherExample.test.tsx b/web/src/WeatherExample.test.tsx index 4ab9d4c..07480fb 100644 --- a/web/src/WeatherExample.test.tsx +++ b/web/src/WeatherExample.test.tsx @@ -7,10 +7,9 @@ import { QueryClientProvider, } from '@tanstack/react-query'; import { http, HttpResponse } from 'msw'; -import { App } from './App'; import { WeatherExample } from './WeatherExample'; import { WEATHER_URL, fetchWeather } from './features/weather/weather'; -import { cities, exampleCity } from './features/weather/cities'; +import { exampleCity } from './features/weather/cities'; import { server } from './test/server'; import { weatherResponse } from './test/fixtures'; function renderExample() { @@ -20,13 +19,6 @@ function renderExample() { , ); } -function renderApp() { - return render( - - - , - ); -} it('renders validated weather and a stable snapshot', async () => { const { asFragment } = renderExample(); await screen.findByText('14 °C'); @@ -66,22 +58,7 @@ it('shows a loading message until the response arrives', async () => { await screen.findByText('14 °C'); expect(screen.queryByRole('status')).not.toBeInTheDocument(); }); - server.use( - http.get(WEATHER_URL, async () => { - await responseReady; - return HttpResponse.json(weatherResponse); - }), - ); - renderExample(); - try { - expect(screen.getByRole('status')).toHaveTextContent('Henter værdata…'); - expect(screen.queryByRole('alert')).not.toBeInTheDocument(); - } finally { - finishRequest(); - } - await screen.findByText('14 °C'); - expect(screen.queryByRole('status')).not.toBeInTheDocument(); -}); + it('shows an accessible error and loading feedback during a manual retry', async () => { let calls = 0; let finishRetry = () => {}; diff --git a/web/src/data/cities.ts b/web/src/data/cities.ts index df23203..bc92c69 100644 --- a/web/src/data/cities.ts +++ b/web/src/data/cities.ts @@ -1,6 +1,6 @@ import type { City } from '../types/city'; -export const cities: City[] = [ +export const cities = [ { id: 'oslo', name: 'Oslo', @@ -10,8 +10,8 @@ export const cities: City[] = [ { id: 'bergen', name: 'Bergen', - latitude: 60.3913, - longitude: 5.3221, + latitude: 60.393, + longitude: 5.3242, }, { id: 'trondheim', @@ -31,4 +31,26 @@ export const cities: City[] = [ latitude: 69.6492, longitude: 18.9553, }, -]; \ No newline at end of file + { + id: 'bodo', + name: 'Bodø', + latitude: 67.28, + longitude: 14.405, + }, + { + id: 'kristiansand', + name: 'Kristiansand', + latitude: 58.1467, + longitude: 7.9956, + }, + { + id: 'alesund', + name: 'Ålesund', + latitude: 62.4722, + longitude: 6.1495, + }, +] as const satisfies readonly City[]; + +export type { CityId } from '../types/city'; + +export const exampleCity = cities[2]; \ No newline at end of file diff --git a/web/src/features/weather/cities.ts b/web/src/features/weather/cities.ts index 04dc116..a1a3aea 100644 --- a/web/src/features/weather/cities.ts +++ b/web/src/features/weather/cities.ts @@ -1,18 +1,3 @@ -export const cities = [ - { id: 'oslo', name: 'Oslo', latitude: 59.9139, longitude: 10.7522 }, - { id: 'bergen', name: 'Bergen', latitude: 60.393, longitude: 5.3242 }, - { id: 'trondheim', name: 'Trondheim', latitude: 63.4305, longitude: 10.3951 }, - { id: 'stavanger', name: 'Stavanger', latitude: 58.97, longitude: 5.7331 }, - { id: 'tromso', name: 'Tromsø', latitude: 69.6492, longitude: 18.9553 }, - { id: 'bodo', name: 'Bodø', latitude: 67.28, longitude: 14.405 }, - { - id: 'kristiansand', - name: 'Kristiansand', - latitude: 58.1467, - longitude: 7.9956, - }, - { id: 'alesund', name: 'Ålesund', latitude: 62.4722, longitude: 6.1495 }, -] as const; -export type City = (typeof cities)[number]; -export type CityId = City['id']; -export const exampleCity = cities[2]; +export { cities, exampleCity } from '../../data/cities'; +export type { CityId } from '../../data/cities'; +export type { City } from '../../types/city'; \ No newline at end of file diff --git a/web/src/type/city.ts b/web/src/type/city.ts deleted file mode 100644 index fca4ad6..0000000 --- a/web/src/type/city.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface City { - id: string; - name: string; - latitude: number; - longitude: number; -} \ No newline at end of file diff --git a/web/src/type/activity.ts b/web/src/types/activity.ts similarity index 100% rename from web/src/type/activity.ts rename to web/src/types/activity.ts diff --git a/web/src/types/city.ts b/web/src/types/city.ts new file mode 100644 index 0000000..e3751c9 --- /dev/null +++ b/web/src/types/city.ts @@ -0,0 +1,16 @@ +export type CityId = + | 'oslo' + | 'bergen' + | 'trondheim' + | 'stavanger' + | 'tromso' + | 'bodo' + | 'kristiansand' + | 'alesund'; + +export interface City { + id: CityId; + name: string; + latitude: number; + longitude: number; +} \ No newline at end of file diff --git a/web/src/type/weather.ts b/web/src/types/weather.ts similarity index 100% rename from web/src/type/weather.ts rename to web/src/types/weather.ts From e57f698dcf5577dca3116482c8bbdfd484db04f6 Mon Sep 17 00:00:00 2001 From: Ole Stangeland Date: Thu, 17 Sep 2026 23:40:57 +0200 Subject: [PATCH 20/21] Issue 14: lage responsivt design med plain css (#26) * refactor: reorganize WeatherCard into component directory * refactor: organize global styles and separate app-specific CSS * feat: add weather card styling * refactor: move WeatherList to components and apply global styles * feature: update weather details dialog styling and small restructuring of content * style(header): update header structure and styling * style(footer): styled footer and removed layout breaking outdated code from stlyes * fix: deleted temporary failing containing outdated styling * fix(header): fixed floating header on big screens and renamed to weathercard --- web/src/App.css | 193 ++++++++ web/src/App.tsx | 157 +++--- web/src/WeatherList.test.tsx | 2 +- .../components/WeatherCard/WeatherCard.css | 124 +++++ .../WeatherCard}/WeatherCard.tsx | 104 ++-- .../components/WeatherList/WeatherList.css | 6 + .../WeatherList}/WeatherList.tsx | 9 +- web/src/styles.css | 445 +++++++++--------- 8 files changed, 693 insertions(+), 347 deletions(-) create mode 100644 web/src/App.css create mode 100644 web/src/components/WeatherCard/WeatherCard.css rename web/src/{ => components/WeatherCard}/WeatherCard.tsx (59%) create mode 100644 web/src/components/WeatherList/WeatherList.css rename web/src/{ => components/WeatherList}/WeatherList.tsx (86%) diff --git a/web/src/App.css b/web/src/App.css new file mode 100644 index 0000000..148c1e1 --- /dev/null +++ b/web/src/App.css @@ -0,0 +1,193 @@ + +/**********/ +/* Footer */ +/**********/ + +.app-footer { + background-color: var(--color-footer-background); + width: 100%; + + margin-block-start: var(--space-lg); + + padding-block: var(--space-md); +} + +.app-footer > div { + width: var(--main-wrapper); + + margin-inline: auto; +} + +/**********/ +/* Header */ +/**********/ + +.app-header { + background-color: var(--color-header-background); + + border-radius: var(--radius-md); + + box-shadow: var(--shadow-md); + + padding: var(--space-md); + + margin-block-start: var(--space-sm); + margin-block-end: var(--space-lg); + + display: grid; + + grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr)); + + gap: var(--space-lg); +} + +.app-header__title-container { + display: flex; + flex-direction: column; +} + +.app-header__weather-controls-section { + display: flex; + flex-direction: column; + + gap: var(--space-md); +} + +.app-header__weather-controls-title { + font-weight: var(--weight-medium); + font-size: var(--font-md); +} + +.app-header__title-container p { + color: var(--color-text-muted); +} + +.app-header__weather-controls { + display: flex; + flex-wrap: wrap; + gap: var(--space-md); +} + +.app-header__weather-controls label { + display: flex; + flex-direction: column; + gap: var(--space-xs); + + flex: 1 1 10rem; +} + + +.app-header__weather-controls select { + width: 100%; +} + +/*******************/ +/* Weather Details */ +/*******************/ + +.details-overlay { + position: fixed; + inset: 0; + z-index: 100; + + display: grid; + place-items: center; + + padding: var(--space-md); + + background: rgb(23 61 53 / 35%); +} + +.details-backdrop { + position: absolute; + inset: 0; + + width: 100%; + height: 100%; + border: 0; + padding: 0; + + background: transparent; +} + +.details-dialog { + position: relative; + z-index: 1; + + width: min(42rem, calc(100vw - 2rem)); + max-height: calc(100vh - 2rem); + overflow: auto; + + background-color: var(--color-background); + + padding: var(--space-lg); + + border-radius: var(--radius-lg); + + border: 1px solid var(--color-border); + + box-shadow: var(--shadow-lg); +} + +.details-dialog__close-button { + width: 100%; + + padding: var(--space-xs); + + border-radius: var(--radius-md); + + background-color: var(--color-action-surface); + border: 1px solid var(--color-action-surface-border); +} + +.details-dialog__close-button:hover, +.details-dialog__close-button:focus-visible { + background-color: var(--color-action-surface-hover); +} + +.details-dialog__close-button:active { + background-color: var(--color-action-surface-active); +} + +.details-dialog h2 { + font-size: var(--font-xl); +} + +.details-dialog__temperature-condition { + display: flex; + flex-direction: column; +} + +.details-dialog__temperature-condition > p:first-child { + font-size: var(--font-lg); + font-weight: var(--weight-semibold); +} + +.details-dialog__temperature-condition > p:last-child { + color: var(--color-text-muted); + font-weight: var(--weight-medium); +} + +.details-dialog__detail-row { + display: flex; + gap: var(--space-xs); +} + +.details-dialog__detail-row dd { + font-weight: var(--weight-medium); +} + +.details-dialog__detail-row dt { + color: var(--color-text-secondary); +} + +.details-dialog__update-time { + color: var(--color-text-muted); + + font-size: var(--font-sm); +} + +.details-dialog__section { + display: grid; + gap: var(--space-sm) +} \ No newline at end of file diff --git a/web/src/App.tsx b/web/src/App.tsx index 1cd4222..fd69f3a 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -10,7 +10,8 @@ import { } from './features/weather/cities'; import { weatherQuery, type CityWeather } from './features/weather/weather'; import { useStoredState } from './shared/storage'; -import { WeatherList, type WeatherListItem } from './WeatherList'; +import { WeatherList, type WeatherListItem } from './components/WeatherList/WeatherList'; +import './App.css'; import { activities } from './data/activities'; import type { ActivityId } from './types/activity'; @@ -47,52 +48,57 @@ onActivityChange: (activity: ActivityId) => void; onSortChange: (sortMode: SortMode) => void; }) { return ( -
-

IT2810 · Gruppe 31

-

Turvær Norge

-

Finn været for din neste tur.

-
- - - +
+
+

Turvær Norge

+

IT2810 · Gruppe 31

+
+ +
+

Finn været for din neste tur.

+
+ + + +
); @@ -289,17 +295,14 @@ function MainWeather({ }) { return (
-
+
-

Valgt by

+

Valgt by

Været i {city.name}

-

Vurdert for{' '} @@ -316,36 +319,36 @@ function MainWeather({ )} {weather && ( <> -

-
-

+

+
+

{weather.current.temperature_2m}°

+

{weatherTypes[weather.current.weather_code] ?? 'Ukjent værtype'}

-

{weather.current.temperature_2m}°

-

- Oppdatert{' '} - -

-
-
-
Nedbør
+
+
+
Nedbør:
{weather.current.precipitation} mm
-
-
Vind
+
+
Vind:
{weather.current.wind_speed_10m} m/s
-
-
Intervall
-
{weather.current.interval / 60} min
-
+

+ Oppdatert{' '} + +

)} + +
); } @@ -437,6 +440,7 @@ export function App() { ); return ( + <>
-
+
+ -
+ + + ); } diff --git a/web/src/WeatherList.test.tsx b/web/src/WeatherList.test.tsx index 2c58173..c243951 100644 --- a/web/src/WeatherList.test.tsx +++ b/web/src/WeatherList.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import { vi, expect, it } from 'vitest'; -import { WeatherList } from './WeatherList'; +import { WeatherList } from './components/WeatherList/WeatherList'; import { exampleCity } from './features/weather/cities'; it('shows feedback when there are no weather cards to display', () => { diff --git a/web/src/components/WeatherCard/WeatherCard.css b/web/src/components/WeatherCard/WeatherCard.css new file mode 100644 index 0000000..ca3873f --- /dev/null +++ b/web/src/components/WeatherCard/WeatherCard.css @@ -0,0 +1,124 @@ +h3 { + font-size: var(--font-xl) +} + +.weather-card { + display: grid; + gap: var(--space-sm); + + background-color: var(--color-card-background); + + padding-inline: var(--space-lg); + padding-block-start: var(--space-sm); + padding-block-end: var(--space-lg); + + border-radius: var(--radius-lg); + border: 1px solid var(--color-card-border); +} + +.weather-card:hover, +.weather-card:focus-visible { + border-color: var(--color-card-border-hover); + background-color: var(--color-card-hover); +} + +.weather-card-selected { + border-color: var(--color-card-border-selected); + background-color: var(--color-card-selected); + + border-width: 2px; +} + +.weather-card__header { + display: flex; + flex-wrap: wrap; + + justify-content: space-between; + align-items: center; +} + +.weather-card__favorite-button { + background-color: transparent; + border: 0; + + font-size: var(--control-font-2xl); +} + +.weather-card__favorite-button:hover, +.weather-card__favorite-button:focus-visible { + border-radius: var(--radius-lg); + + background-color: var(--color-action-surface-hover); + + border: 1px solid var(--color-border-hover); +} + +.weather-card__favorite-button:active { + background-color: var(--color-action-surface-active); +} + +.weather-card__button-full-width { + width: 100%; + + padding: var(--space-xs); + + border-radius: var(--radius-md); + + background-color: var(--color-action-surface); + border: 1px solid var(--color-action-surface-border); +} + +.weather-card__button-full-width:hover, +.weather-card__button-full-width:focus-visible { + background-color: var(--color-action-surface-hover); +} + +.weather-card__button-full-width:active { + background-color: var(--color-action-surface-active); +} + +.weather-card__temperature-condition { + display: flex; + flex-direction: column; +} + +.weather-card__temperature-condition > div:first-child { + font-size: var(--font-lg); + font-weight: var(--weight-semibold); +} + +.weather-card__temperature-condition > div:last-child { + color: var(--color-text-muted); + font-weight: var(--weight-medium); +} + +.weather-card__details { + display: grid; + gap: var(--space-sm); +} + +.weather-card__detail-row { + display: flex; + gap: var(--space-xs); +} + +.weather-card__detail-row dd { + font-weight: var(--weight-medium); +} + +.weather-card__detail-row dt { + color: var(--color-text-secondary); +} + +.weather-card__rank { + padding: var(--space-xs); + + background-color: var(--color-action-surface); + + border-radius: var(--radius-md); +} + +.weather-card__weather-error { + display: grid; + gap: var(--space-sm); +} \ No newline at end of file diff --git a/web/src/WeatherCard.tsx b/web/src/components/WeatherCard/WeatherCard.tsx similarity index 59% rename from web/src/WeatherCard.tsx rename to web/src/components/WeatherCard/WeatherCard.tsx index 71a375e..1114a80 100644 --- a/web/src/WeatherCard.tsx +++ b/web/src/components/WeatherCard/WeatherCard.tsx @@ -1,5 +1,6 @@ -import type { City } from './features/weather/cities'; -import type { CityWeather } from './features/weather/weather'; +import type { City } from '../../features/weather/cities'; +import type { CityWeather } from '../../features/weather/weather'; +import './WeatherCard.css'; export type WeatherCardProps = { city: City; @@ -63,40 +64,32 @@ export function WeatherCard({ isSelected ? 'weather-card weather-card-selected' : 'weather-card' } > -

{city.name}

+
+

{city.name}

+ + +
+ {rank !== null && ( -

#{rank} for valgt aktivitet

+

#{rank} for valgt aktivitet

)} - - - + {isLoading &&

Henter værdata…

} + {hasError && ( -
+

Kunne ikke hente værdata.

)} {weather && ( -
-
-
Værtype
-
{weatherType}
-
-
-
Temperatur
-
{weather.current.temperature_2m} °C
+
+
+
+
Temperatur
+
{weather.current.temperature_2m} °C
+
+ +
+
Værtype
+
{weatherType}
+
-
-
Nedbør
+ +
+
Nedbør:
{weather.current.precipitation} mm
-
-
Vind
+
+
Vind:
{weather.current.wind_speed_10m} m/s
)} + + + + ); } diff --git a/web/src/components/WeatherList/WeatherList.css b/web/src/components/WeatherList/WeatherList.css new file mode 100644 index 0000000..e9f2296 --- /dev/null +++ b/web/src/components/WeatherList/WeatherList.css @@ -0,0 +1,6 @@ +.weather-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(var(--weather-card-min-width), 1fr)); + + gap: var(--space-md); +} \ No newline at end of file diff --git a/web/src/WeatherList.tsx b/web/src/components/WeatherList/WeatherList.tsx similarity index 86% rename from web/src/WeatherList.tsx rename to web/src/components/WeatherList/WeatherList.tsx index 865b301..77d2dc0 100644 --- a/web/src/WeatherList.tsx +++ b/web/src/components/WeatherList/WeatherList.tsx @@ -1,7 +1,8 @@ import type { ReactNode } from 'react'; -import type { City } from './features/weather/cities'; -import type { CityWeather } from './features/weather/weather'; -import { WeatherCard } from './WeatherCard'; +import type { City } from '../../features/weather/cities'; +import type { CityWeather } from '../../features/weather/weather'; +import { WeatherCard } from '../WeatherCard/WeatherCard'; +import './WeatherList.css'; export type WeatherListItem = { city: City; @@ -35,7 +36,7 @@ export function WeatherList({ }: WeatherListProps) { return (
-

Vær i norske byer

+

Været i norske byer

{items.length === 0 ? (

Ingen værkort å vise.

diff --git a/web/src/styles.css b/web/src/styles.css index 02670f1..fe88d11 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1,247 +1,246 @@ -:root { - font-family: system-ui, sans-serif; - line-height: 1.6; - color: #173d35; - background: #f2f5f0; -} + +/********************************************************************/ +/* Includes: CSS resets, CSS variables, Utilities, and Base Layout */ +/********************************************************************/ + + +/**************/ +/* CSS resets */ +/**************/ + * { box-sizing: border-box; } + body { margin: 0; + line-height: 1.5; + min-height: 100dvh; } -main { - width: min(1120px, 100% - 2rem); - margin: 2rem auto 4rem; -} -h1 { - font-size: clamp(2rem, 6vw, 3rem); - line-height: 1.1; -} -.app-header { - display: flex; - justify-content: space-between; - gap: 2rem; - align-items: end; - margin-bottom: 2rem; -} -.eyebrow { - margin: 0; - color: #9a491c; - font-size: 0.8rem; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; + +/* Responsive media by default */ +img, +picture, +video, +canvas, +svg { + display: block; + max-width: 100%; } -.controls { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; -} -label { - display: grid; - gap: 0.25rem; - font-size: 0.85rem; - font-weight: 600; -} -select { - min-height: 44px; - padding: 0.5rem 2rem 0.5rem 0.65rem; - border: 1px solid #bacac1; - border-radius: 0.25rem; - background: #fff; - color: inherit; + +/* Make form controls inherit font */ +button, +input, +select, +textarea { font: inherit; } -section { - border: 1px solid #bacac1; - background: #fff; - padding: clamp(1rem, 4vw, 2rem); - border-radius: 0.5rem; -} -.main-weather { - margin-bottom: 1.5rem; -} -.section-heading { - display: flex; - align-items: start; - justify-content: space-between; - gap: 1rem; -} -.section-heading h2 { - margin: 0.25rem 0 0; -} -.resource-navigation { - display: flex; - gap: 0.5rem; + +/* Avoid text overflowing containers */ +p, +h1, +h2, +h3, +h4 { + overflow-wrap: break-word; } -.main-weather-summary { - display: flex; - justify-content: space-between; - align-items: end; - border-block: 1px solid #d5e2dc; - margin: 1.5rem 0; - padding: 1rem 0; -} -.weather-type, -.update-time { + +/* Remove default margins */ +h1, +h2, +h3, +h4, +p, +figure, +blockquote, +dl, +dd { margin: 0; } -.weather-type { - font-weight: 700; -} -.temperature { - margin: 0.25rem 0 0; - font-size: clamp(3rem, 8vw, 5rem); - line-height: 1; - font-weight: 700; + +/*****************/ +/* CSS variables */ +/*****************/ + +:root { + /* Surfaces */ + --color-background: #f2f5f0; + --color-surface: #ffffff; + --color-surface-hover: #f8fafc; + --color-surface-selected: #f1f5f9; + + /* Text Color */ + --color-text: #173d35; + --color-text-secondary: #475569; + --color-text-muted: #64748b; + --color-text-inverse: #ffffff; + + /* Borders */ + --color-border: #e2e8f0; + --color-border-hover: #cbd5e1; + --color-border-focus: #3b82f6; + + /* Primary */ + --color-primary: #398d4b; + --color-primary-hover: #327d42; + --color-primary-active: #296c38; + --color-primary-selected: #398d4b; + + --color-primary-focus: #2563eb; + --color-primary-text: #ffffff; + + /* Secondary */ + --color-action-surface: #ddeaf7; + --color-action-surface-hover: #bdcadb; + --color-action-surface-active: #cbd5e1; + + --color-action-surface-border: #b2cbe4; + + --color-action-text: #1e293b; + --color-action-focus: #2563eb; + + /* Header */ + --color-header-background: #f3f9ff; + + /* Footer */ + --color-footer-background: #afc4d5; + + /* Cards */ + --color-card-background: #ffffff; + --color-card-hover: #f8fafc; + --color-card-selected: #f3f9fc; + + --color-card-border: #e2e8f0; + --color-card-border-hover: #cbd5e1; + --color-card-border-selected: #94a3b8; + --color-card-focus: #2563eb; + + /* Weather Card */ + --weather-card-min-width: 17rem; + + /* Spacing */ + --space-xs: clamp(0.25rem, 0.2rem + 0.2vw, 0.5rem); + --space-sm: clamp(0.5rem, 0.4rem + 0.3vw, 0.75rem); + --space-md: clamp(1rem, 0.8rem + 0.5vw, 1.5rem); + --space-lg: clamp(1.5rem, 1.1rem + 1vw, 2.5rem); + --space-xl: clamp(2rem, 1.2rem + 2vw, 4rem); + --space-2xl: clamp(3rem, 1.5rem + 4vw, 7rem); + + /* Padding */ + --padding-xs: clamp(0.375rem, 0.3rem + 0.2vw, 0.5rem); + --padding-sm: clamp(0.5rem, 0.4rem + 0.3vw, 0.75rem); + --padding-md: clamp(0.75rem, 0.5rem + 0.7vw, 1.25rem); + --padding-lg: clamp(1rem, 0.5rem + 1.5vw, 2rem); + --padding-xl: clamp(1.5rem, 0.5rem + 3vw, 4rem); + + /* Font Size */ + --font-xs: clamp(0.75rem, 0.72rem + 0.1vw, 0.8rem); + --font-sm: clamp(0.875rem, 0.84rem + 0.1vw, 0.95rem); + --font-md: clamp(1rem, 0.95rem + 0.2vw, 1.125rem); + --font-lg: clamp(1.125rem, 1rem + 0.4vw, 1.375rem); + --font-xl: clamp(1.5rem, 1.2rem + 1vw, 2rem); + --font-2xl: clamp(2rem, 1.4rem + 2vw, 3.25rem); + --font-3xl: clamp(2.5rem, 1.5rem + 4vw, 5rem); + + /* Font Weight */ + --weight-normal: 400; + --weight-medium: 500; + --weight-semibold: 600; + --weight-bold: 700; + + /* Radius */ + --radius-xs: 0.25rem; + --radius-sm: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + --radius-xl: 1rem; + --radius-2xl: 1.5rem; + --radius-full: 9999px; + + /* Interactice Controls Font Size */ + --control-font-sm: 0.875rem; + --control-font-md: 1rem; + --control-font-lg: 1.25rem; + --control-font-xl: 1.5rem; + --control-font-2xl: 2rem; + --control-font-3xl: 2.7rem; + + /* Bow Shadow */ + --shadow-sm: 0 1px 3px rgb(0 0 0 / 10%); + --shadow-md: 0 4px 12px rgb(0 0 0 / 15%); + --shadow-lg: 0 12px 32px rgb(0 0 0 / 20%); + + /* Wrappers */ + --main-wrapper: min(1120px, 100% - 2rem); } -.weather-details { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 1rem; + +/*************/ +/* Utilities */ +/*************/ + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; } -.weather-details dd { - font-size: 1.3rem; + +.hidden { + display: none; } -.weather-list { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); - gap: 1rem; + +/* Margin block */ +.margin-block-start-sm { + margin-block-start: var(--space-sm); } -.weather-card-slot { - min-width: 0; + +.margin-block-start-md { + margin-block-start: var(--space-md); } -.weather-card-slot-expanded { - position: relative; - z-index: 10; + +.margin-block-start-lg { + margin-block-start: var(--space-lg); } -.expanded-weather { - width: 100%; - margin: 0; + +.margin-block-end-sm { + margin-block-end: var(--space-sm); } -.details-overlay { - position: fixed; - inset: 0; - z-index: 100; - display: grid; - place-items: center; - padding: 1rem; - background: rgb(23 61 53 / 35%); + +.margin-block-end-md { + margin-block-end: var(--space-md); } -.details-backdrop { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - min-height: 0; - padding: 0; - border: 0; - background: transparent; - cursor: default; -} -.details-dialog { - position: relative; - z-index: 1; - width: min(42rem, 100%); - max-height: calc(100vh - 2rem); - overflow: auto; - border-radius: 0.5rem; - box-shadow: 0 1rem 2.5rem rgb(23 61 53 / 25%); -} -.expanded-weather .resource-navigation { - flex-wrap: wrap; - justify-content: flex-end; -} -.weather-card { - border: 1px solid #bacac1; - border-radius: 0.5rem; - padding: 1rem; - background: #fff; - cursor: pointer; -} -.weather-card button + button { - margin-left: 0.5rem; -} -.weather-card:hover, -.weather-card:focus-visible, -.weather-card-selected { - border-color: #173d35; - box-shadow: 0 0 0 3px #d5e2dc; -} -.weather-card h3 { - margin-top: 0; -} -.weather-rank { - margin: -0.5rem 0 0.75rem; - color: #9a491c; - font-size: 0.8rem; - font-weight: 700; -} -.weather-card dl { - gap: 0.75rem 1rem; -} -.weather-card dd { - font-size: 1.1rem; -} -dl { - display: flex; - flex-wrap: wrap; - gap: 1.5rem; + +.margin-block-end-lg { + margin-block-end: var(--space-lg); } -dt { - font-size: 0.85rem; + + +/***************/ +/* Base layout */ +/***************/ + +main { + width: var(--main-wrapper); + margin-inline: auto; + + margin-block-start: var(--space-md); + + /* Fill any unused vertical space so the footer is pushed down */ + flex: 1; } -dd { - margin: 0.25rem 0; - font-size: 1.5rem; - font-weight: 600; + +:root { + font-family: system-ui, sans-serif; + color: var(--color-text); + background: var(--color-background); } -button { - font: inherit; - min-height: 44px; - padding: 0.5rem 1rem; - background: #173d35; - color: white; - border: 0; - border-radius: 0.25rem; - cursor: pointer; -} - -button[aria-pressed='true'] { - background: #9a491c; -} -a { - color: inherit; - text-underline-offset: 0.2em; -} -:focus-visible { - outline: 3px solid #9a491c; - outline-offset: 4px; -} -footer { - margin-top: 2rem; - font-size: 0.85rem; -} -@media (max-width: 760px) { - .app-header, - .section-heading, - .main-weather-summary { - align-items: stretch; - flex-direction: column; - } - .controls, - .controls label, - .controls select { - width: 100%; - } - .weather-details { - grid-template-columns: 1fr; - } - .resource-navigation button { - flex: 1; - } - .details-dialog { - width: 100%; - } + +/* Make the React app at least as tall as the viewport, and stack main + footer vertically */ +#root { + min-height: 100dvh; + display: flex; + flex-direction: column; } + From 3c4699d4ffd47510b27effbb39946afdfb6c439a Mon Sep 17 00:00:00 2001 From: Oliver Dragland Date: Fri, 18 Sep 2026 11:39:00 +0200 Subject: [PATCH 21/21] feature(ci): sett opp runners og deploy til VM (#28) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feature(ci): sett opp runners og deploy til VM * fix(ci): bruk artifact-versjonen på NTNU GitHub --------- Co-authored-by: Oliver Dragland --- .github/actionlint.yaml | 4 ++ .github/workflows/ci.yml | 120 ++++++++++++++++++++++++++++++++ deploy/apache-project1.conf | 15 ++++ scripts/deploy-project1.sh | 96 +++++++++++++++++++++++++ scripts/test-deploy-project1.sh | 99 ++++++++++++++++++++++++++ 5 files changed, 334 insertions(+) create mode 100644 .github/actionlint.yaml create mode 100644 .github/workflows/ci.yml create mode 100644 deploy/apache-project1.conf create mode 100644 scripts/deploy-project1.sh create mode 100644 scripts/test-deploy-project1.sh diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..9e78517 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,4 @@ +self-hosted-runner: + labels: + - t31-ci + - t31-deploy diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7e8bf78 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,120 @@ +name: CI + +on: + push: + branches: [dev, main] + pull_request: + branches: [dev, main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + # la main bli ferdig, også hvis vi må rulle tilbake + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +defaults: + run: + shell: bash + +jobs: + quality: + name: Quality checks + runs-on: [self-hosted, linux, x64, t31-ci] + timeout-minutes: 30 + env: + CI: 'true' + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .node-version + + - name: Use the pinned npm version + run: npm install --global npm@11.11.1 + + - name: Install dependencies + run: npm ci + + - name: Check code, contracts, migration metadata, tests and builds + run: npm run check + + - name: Test release switching and rollback + run: bash scripts/test-deploy-project1.sh + + - name: Install test browsers + run: npm run test:e2e:install + + - name: Run browser checks + run: npm run test:e2e -- --workers=2 + + - name: Preserve browser failure evidence + if: failure() + # denne node20-versjonen finnes på git.ntnu.no og støtter enterprise + uses: actions/upload-artifact@v3.2.1-node20 + with: + name: browser-results-${{ github.run_id }}-${{ github.run_attempt }} + path: test-results/ + retention-days: 7 + + - name: Save the tested production build + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + uses: actions/upload-artifact@v3.2.1-node20 + with: + name: project1-${{ github.sha }}-${{ github.run_attempt }} + path: web/dist/ + if-no-files-found: error + retention-days: 7 + + deploy: + name: Deploy Project 1 + needs: quality + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + runs-on: [self-hosted, linux, x64, t31-deploy] + timeout-minutes: 10 + environment: + name: production + url: http://it2810-31.idi.ntnu.no/project1/ + concurrency: + group: project1-production + cancel-in-progress: false + steps: + - name: Check that this commit is still the tip of main + id: latest + uses: actions/github-script@v7 + with: + script: | + const { data } = await github.rest.repos.getCommit({ + ...context.repo, + ref: 'main', + }); + core.setOutput('current', data.sha === context.sha); + if (data.sha !== context.sha) { + core.notice('A newer commit is on main; skip this outdated deployment.'); + } + + - name: Check out the deployment script from the tested commit + if: steps.latest.outputs.current == 'true' + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Download the tested production build + if: steps.latest.outputs.current == 'true' + uses: actions/download-artifact@v3-node20 + with: + name: project1-${{ github.sha }}-${{ github.run_attempt }} + path: web/dist/ + + - name: Publish to Apache and verify the release + if: steps.latest.outputs.current == 'true' + env: + RELEASE_ID: ${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }} + run: bash scripts/deploy-project1.sh web/dist "$RELEASE_ID" diff --git a/deploy/apache-project1.conf b/deploy/apache-project1.conf new file mode 100644 index 0000000..53e2bf4 --- /dev/null +++ b/deploy/apache-project1.conf @@ -0,0 +1,15 @@ +# slå på med a2enconf t31-project1, sjekk configen og last apache på nytt +RedirectMatch 302 ^/project1$ /project1/ +Alias /project1/ /srv/t31-project1/current/ + + + Options +FollowSymLinks -Indexes -ExecCGI -Includes + AllowOverride None + Require all granted + DirectoryIndex index.html + # prosjekt 1 er bare statiske filer, ikke kjør noe backend her + SetHandler default-handler + + Header set Cache-Control "no-cache" + + diff --git a/scripts/deploy-project1.sh b/scripts/deploy-project1.sh new file mode 100644 index 0000000..41111cc --- /dev/null +++ b/scripts/deploy-project1.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# kjør som t31-deploy, uten sudo. apache leser fra current +# ryddet opp med hjelp fra codex +set -Eeuo pipefail +umask 022 + +fail() { + printf '%s\n' "$*" >&2 + exit 1 +} + +check_input() { + [[ $release_id =~ ^[a-f0-9]{40}-[0-9]+-[0-9]+$ ]] || fail 'ugyldig release-id' + [[ -d $deploy_root/releases && -w $deploy_root ]] || fail 'deploy-mappa må settes opp først' + [[ -s $source_dir/index.html && -d $source_dir/assets ]] || fail 'bygget mangler index.html eller assets' + [[ -z $(find "$source_dir" -mindepth 1 ! -type f ! -type d -print -quit) ]] || fail 'bygget kan bare inneholde vanlige filer og mapper' + [[ ! -e $deploy_root/current || -L $deploy_root/current ]] || fail 'current må være en symlink' + [[ $health_url == http://* || $health_url == https://* ]] || fail 'sjekk-adressen må bruke http eller https' +} + +# bytt hele siden på en gang, så ingen får et halvferdig bygg +set_current() { + rm -f -- "$work_dir/current" + ln -s -- "$1" "$work_dir/current" + mv -Tf -- "$work_dir/current" "$deploy_root/current" +} + +cleanup() { + local status=$? + trap - EXIT HUP INT TERM + + # noe feilet etter byttet? legg tilbake forrige versjon + if [[ $status != 0 && $switched == true ]]; then + if [[ -n $previous ]]; then + set_current "$previous" + printf 'deploy feilet, gikk tilbake til %s\n' "$previous" >&2 + else + rm -- "$deploy_root/current" + printf 'første deploy feilet, fjernet current\n' >&2 + fi + fi + rm -rf -- "$work_dir" + exit "$status" +} + +prepare_release() { + mkdir "$work_dir/build" + cp -R -- "$source_dir/." "$work_dir/build/" + printf '%s\n' "$release_id" > "$work_dir/build/release.txt" + find "$work_dir/build" -type d -exec chmod 755 {} + + find "$work_dir/build" -type f -exec chmod 644 {} + + mv -T -- "$work_dir/build" "$release_dir" +} + +# et 200-svar er ikke nok, apache må sende akkurat den fila vi bygde +check_file() { + local file=$1 url_path=${2-$1} + curl --fail --silent --show-error --noproxy '*' --connect-timeout 5 --max-time 15 \ + "$health_url/$url_path" -o "$work_dir/response" || fail "fikk ikke hentet $file fra apache" + cmp -s -- "$release_dir/$file" "$work_dir/response" || fail "apache sender feil innhold for $file" +} + +check_release() { + check_file release.txt + check_file index.html '' # sjekk selve /project1/, ikke bare index.html + find "$release_dir/assets" -type f -printf '%P\0' > "$work_dir/assets" + while IFS= read -r -d '' asset; do + [[ $asset =~ ^[A-Za-z0-9_./-]+$ ]] || fail 'uventet filnavn i assets' + check_file "assets/$asset" + done < "$work_dir/assets" +} + +[[ $# == 2 ]] || fail 'bruk: deploy-project1.sh BYGGMAPPE RELEASE_ID' +source_dir=$(realpath -- "$1") +release_id=$2 +deploy_root=${T31_DEPLOY_ROOT:-/srv/t31-project1} +health_url=${T31_DEPLOY_URL:-http://127.0.0.1/project1/} +health_url=${health_url%/} +check_input + +# bare en deploy av gangen, også om noen kjører scriptet manuelt +exec 9>"$deploy_root/deploy.lock" +flock -w 60 9 || fail 'en annen deploy holder på' +release_dir=$deploy_root/releases/$release_id +[[ ! -e $release_dir ]] || fail 'denne releasen finnes fra før, start en ny kjøring' +previous=$(readlink "$deploy_root/current" || true) +work_dir=$(mktemp -d "$deploy_root/.deploy-XXXXXXXX") +switched=false +trap cleanup EXIT +trap 'exit 1' HUP INT TERM + +prepare_release +switched=true # rollback må være klar før vi bytter +set_current "$release_dir" +check_release +printf 'publisert %s på %s/\n' "$release_id" "$health_url" diff --git a/scripts/test-deploy-project1.sh b/scripts/test-deploy-project1.sh new file mode 100644 index 0000000..4e3edd2 --- /dev/null +++ b/scripts/test-deploy-project1.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# sjekker deploy og rollback med en liten lokal webserver +set -Eeuo pipefail +repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +scratch=$(mktemp -d) +server_pid='' +cleanup() { + if [[ -n $server_pid ]]; then + kill "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + fi + rm -rf -- "$scratch" +} +trap cleanup EXIT +trap 'exit 1' HUP INT TERM +mkdir -p "$scratch/site/releases" "$scratch/build/assets" "$scratch/http" +ln -s "$scratch/site/current" "$scratch/http/project1" +printf '\n' > "$scratch/build/index.html" +printf 'console.log("release one");\n' > "$scratch/build/assets/app.js" + +python3 - "$scratch" <<'PY' & +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +import sys + +root = Path(sys.argv[1]) + +class Handler(SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=str(root / 'http'), **kwargs) + + def do_GET(self): + if ((root / 'bad-index').exists() and self.path in ['/project1/', '/project1/index.html']) or ( + (root / 'bad-asset').exists() and '/assets/' in self.path + ): + self.send_response(200) + self.end_headers() + self.wfile.write(b'wrong release') + else: + super().do_GET() + + def log_message(self, *args): + pass + +server = ThreadingHTTPServer(('127.0.0.1', 0), Handler) +(root / 'port').write_text(str(server.server_port)) +server.serve_forever() +PY +server_pid=$! +for ((attempt=0; attempt<100; attempt++)); do + [[ -s $scratch/port ]] && break + sleep 0.05 +done +[[ -s $scratch/port ]] || { printf 'HTTP server did not start.\n' >&2; exit 1; } + +export T31_DEPLOY_ROOT=$scratch/site +T31_DEPLOY_URL=http://127.0.0.1:$(cat "$scratch/port")/project1/ +export T31_DEPLOY_URL +first=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1 +second=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-2-1 +failed=cccccccccccccccccccccccccccccccccccccccc-3-1 +deploy() { bash "$repo_root/scripts/deploy-project1.sh" "$scratch/build" "$1"; } +expect_failure() { + if deploy "$1" > "$scratch/failure.log" 2>&1; then + printf 'Expected deployment %s to fail.\n' "$1" >&2 + exit 1 + fi +} + +touch "$scratch/bad-index" +expect_failure "$failed" +[[ ! -e $scratch/site/current && ! -L $scratch/site/current ]] +rm "$scratch/bad-index" +deploy "$first" +[[ $(readlink "$scratch/site/current") == "$scratch/site/releases/$first" ]] +cmp "$scratch/build/assets/app.js" "$scratch/site/current/assets/app.js" +[[ $(stat -c '%a' "$scratch/site/current/index.html") == 644 ]] + +printf 'console.log("release two");\n' > "$scratch/build/assets/app.js" +deploy "$second" +[[ -f $scratch/site/releases/$first/index.html ]] +[[ $(readlink "$scratch/site/current") == "$scratch/site/releases/$second" ]] +expect_failure "$second" +expect_failure '../../escape' +ln -s /etc/passwd "$scratch/build/assets/link" +expect_failure "${failed%1}2" +rm "$scratch/build/assets/link" + +touch "$scratch/bad-index" +expect_failure "${failed%1}3" +[[ $(readlink "$scratch/site/current") == "$scratch/site/releases/$second" ]] +rm "$scratch/bad-index" +touch "$scratch/bad-asset" +expect_failure "${failed%1}4" +[[ $(readlink "$scratch/site/current") == "$scratch/site/releases/$second" ]] +rm "$scratch/bad-asset" +curl --fail --silent --noproxy '*' "$T31_DEPLOY_URL/release.txt" | cmp - "$scratch/site/current/release.txt" +[[ -z $(find "$scratch/site" -maxdepth 1 -name '.deploy-*' -print -quit) ]] +printf 'Deployment checks passed: first release, update, validation, index/asset rollback and cleanup.\n'