diff --git a/.dockerignore b/.dockerignore index 65f3dcd0..f3e717c0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,7 @@ node_modules client/node_modules server/node_modules client/dist +shared/dist data uploads .git diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ef038083..21e518bf 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -102,16 +102,15 @@ jobs: echo "VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT echo "$STABLE → $NEW_VERSION ($BUMP)" - # Update package.json files and Helm chart - cd server && npm version "$NEW_VERSION" --no-git-tag-version && cd .. - cd client && npm version "$NEW_VERSION" --no-git-tag-version && cd .. + # Update all workspace + root package.json files and the root lockfile in one shot + npm version "$NEW_VERSION" --workspaces --include-workspace-root --no-git-tag-version sed -i "s/^version: .*/version: $NEW_VERSION/" charts/trek/Chart.yaml sed -i "s/^appVersion: .*/appVersion: \"$NEW_VERSION\"/" charts/trek/Chart.yaml # Commit and tag git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add server/package.json server/package-lock.json client/package.json client/package-lock.json charts/trek/Chart.yaml + git add package.json package-lock.json server/package.json client/package.json shared/package.json charts/trek/Chart.yaml git commit -m "chore: bump version to $NEW_VERSION [skip ci]" git tag "v$NEW_VERSION" git push origin main --follow-tags diff --git a/.github/workflows/lint-prettier.yml b/.github/workflows/lint-prettier.yml new file mode 100644 index 00000000..41c74611 --- /dev/null +++ b/.github/workflows/lint-prettier.yml @@ -0,0 +1,53 @@ +name: Lint & Prettier +on: + pull_request: + branches: [main, dev] +jobs: + lint: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '24' + + - name: Install dependencies + run: npm install + + - name: Run lint & format check + id: checks + continue-on-error: true + run: | + cd shared + npm run lint + npm run format:check + + - name: Comment on PR if checks failed + if: steps.checks.outcome == 'failure' + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: [ + '## ❌ Lint & Prettier check failed', + '', + 'Please fix the issues locally by running the following commands inside the `shared` package:', + '', + '```bash', + 'cd shared', + 'npm run lint', + 'npm run format', + '```', + '', + 'Then commit and push the changes.', + ].join('\n'), + }); + + - name: Fail the job if checks failed + if: steps.checks.outcome == 'failure' + run: exit 1 \ No newline at end of file diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 9cc8577d..88755200 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -34,4 +34,5 @@ jobs: command: cves image: trek:scan only-severities: critical,high + only-fixed: true exit-code: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6c11b481..20f1864b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,10 +8,47 @@ on: branches: [main, dev] paths: - 'server/**' - - '.github/workflows/test.yml' - 'client/**' + - 'shared/**' + - '.github/workflows/test.yml' jobs: + i18n-parity: + name: i18n Key Parity + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: 24 + + - name: Check i18n key parity + run: node shared/scripts/i18n-parity.mjs --strict + + shared-contracts: + name: Shared Contracts (Zod) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + cache-dependency-path: package-lock.json + + - name: Install dependencies + run: npm ci --workspace shared + + - name: Typecheck + run: cd shared && npm run typecheck + + - name: Run tests + run: cd shared && npm test + server-tests: name: Server Tests runs-on: ubuntu-latest @@ -21,12 +58,33 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 24 cache: npm - cache-dependency-path: server/package-lock.json + cache-dependency-path: package-lock.json - name: Install dependencies - run: cd server && npm ci + run: npm ci + + - name: Ensure @swc/core's Linux binary for unplugin-swc + # The lockfile was generated on Windows and omits @swc/core's Linux + # optional native binary, so npm ci/install skips it on the runner. + # Install the matching version explicitly so the server's SWC transform + # (server/vitest.config.ts) can load. + run: | + SWC_VERSION=$(node -p "require('@swc/core/package.json').version") + npm install --no-save --legacy-peer-deps "@swc/core-linux-x64-gnu@$SWC_VERSION" + + - name: Build shared + run: npm run build --workspace=shared + + - name: Build server (tsc -> dist) + run: cd server && npm run build + + - name: Typecheck + run: cd server && npm run typecheck + + - name: Lint + run: cd server && npm run lint:check - name: Run tests run: cd server && npm run test:coverage @@ -48,12 +106,24 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: 22 + node-version: 24 cache: npm - cache-dependency-path: client/package-lock.json + cache-dependency-path: package-lock.json - name: Install dependencies - run: cd client && npm ci + run: npm ci --workspace shared && npm ci --workspace client + + - name: Build shared + run: npm run build --workspace=shared + + - name: Typecheck + run: cd client && npm run typecheck + + - name: Lint + run: cd client && npm run lint:check + + - name: Page pattern check + run: cd client && npm run lint:pages - name: Run tests run: cd client && npm run test:coverage diff --git a/.gitignore b/.gitignore index c796e687..b1e4bdf2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ node_modules/ # Build output client/dist/ +server/dist/ +shared/dist/ server/public/* !server/public/.gitkeep diff --git a/Dockerfile b/Dockerfile index c16cc7b8..d6463fd7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,97 @@ -# Stage 1: Build React client +# ── Stage 0: gosu ──────────────────────────────────────────────────────────── +# Rebuild gosu with a current Go toolchain so the runtime image ships no stale +# Go stdlib (Debian's apt gosu is built with an old Go that trips CVE scanners). +# The binary and its runtime behaviour are identical to the apt package. +FROM golang:1.25-alpine AS gosu-build +RUN CGO_ENABLED=0 GOBIN=/out go install github.com/tianon/gosu@latest + +# ── Stage 1: shared ────────────────────────────────────────────────────────── +FROM node:24-alpine AS shared-builder +WORKDIR /app +COPY package.json package-lock.json ./ +COPY shared/package.json ./shared/ +RUN npm ci --workspace=shared +COPY shared/ ./shared/ +RUN npm run build --workspace=shared + +# ── Stage 2: client ────────────────────────────────────────────────────────── FROM node:24-alpine AS client-builder -WORKDIR /app/client -COPY client/package*.json ./ -RUN npm ci -COPY client/ ./ -RUN npm run build +WORKDIR /app +COPY package.json package-lock.json ./ +COPY shared/package.json ./shared/ +COPY client/package.json ./client/ +RUN npm ci --workspace=client +COPY --from=shared-builder /app/shared/dist ./shared/dist +COPY client/ ./client/ +RUN npm run build --workspace=client -# Stage 2: Production server -FROM node:24-alpine +# ── Stage 3: server ────────────────────────────────────────────────────────── +# --ignore-scripts skips native builds (better-sqlite3); they happen in the production stage. +FROM node:24-alpine AS server-builder +WORKDIR /app +COPY package.json package-lock.json ./ +COPY shared/package.json ./shared/ +COPY server/package.json ./server/ +RUN npm ci --workspace=server --ignore-scripts +COPY --from=shared-builder /app/shared/dist ./shared/dist +COPY server/ ./server/ +RUN npm run build --workspace=server +# ── Stage 4: production runtime ────────────────────────────────────────────── +FROM node:24-trixie-slim WORKDIR /app -# Timezone support + native deps (better-sqlite3 needs build tools) -COPY server/package*.json ./ -RUN apk add --no-cache tzdata dumb-init su-exec python3 make g++ && \ - npm ci --production && \ - rm package-lock.json && \ - apk del python3 make g++ && \ - rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx +# Workspace manifests only — source never enters this stage. +COPY package.json package-lock.json ./ +COPY shared/package.json ./shared/ +COPY server/package.json ./server/ -COPY server/ ./ -COPY --from=client-builder /app/client/dist ./public -COPY --from=client-builder /app/client/public/fonts ./public/fonts +# better-sqlite3 native addon requires build tools (purged after compile). +# kitinerary-extractor for booking-confirmation import: +# amd64 — static binary from KDE CDN (glibc 2.17+; wget stays for healthcheck) +# arm64 — apt package (KDE publishes no arm64 static binary) +RUN apt-get update && \ + apt-get install -y --no-install-recommends tzdata dumb-init wget ca-certificates python3 build-essential && \ + npm ci --workspace=server --omit=dev && \ + ARCH=$(dpkg --print-architecture) && \ + if [ "$ARCH" = "amd64" ]; then \ + wget -qO /tmp/ki.tgz https://cdn.kde.org/ci-builds/pim/kitinerary/release-26.04/linux/kitinerary-extractor-x86_64-26.04.2.tgz && \ + echo "ba5cfb4a2353157c8f54cbeaea0097c5bf2c3a810e0342f63d6e524826176628 /tmp/ki.tgz" | sha256sum -c && \ + tar -xz -C /usr/local -f /tmp/ki.tgz bin/kitinerary-extractor share/locale && \ + rm /tmp/ki.tgz; \ + else \ + apt-get install -y --no-install-recommends libkitinerary-bin && \ + ln -sf "$(find /usr/lib -name kitinerary-extractor -type f | head -1)" /usr/local/bin/kitinerary-extractor; \ + fi && \ + apt-get purge -y python3 build-essential && \ + apt-get autoremove -y && \ + rm -rf /var/lib/apt/lists/* /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx -RUN rm -f package-lock.json && \ - mkdir -p /app/data/logs /app/uploads/files /app/uploads/covers /app/uploads/avatars /app/uploads/photos && \ - mkdir -p /app/server && ln -s /app/uploads /app/server/uploads && ln -s /app/data /app/server/data && \ +# gosu rebuilt with a current Go toolchain (stage 0) — used by CMD to drop to node. +COPY --from=gosu-build /out/gosu /usr/local/bin/gosu + +ENV XDG_CACHE_HOME=/tmp/kf6-cache +# Prevent Qt from probing for a display in headless containers. +ENV QT_QPA_PLATFORM=offscreen +# Fixed path for both amd64 (static binary) and arm64 (symlink to apt binary). +# Override with KITINERARY_EXTRACTOR_PATH if you install it elsewhere. +ENV KITINERARY_EXTRACTOR_PATH=/usr/local/bin/kitinerary-extractor + +COPY --from=server-builder /app/server/dist ./server/dist +# Runtime data assets read from server/assets at runtime: airports.json (flight +# transport search) and atlas/*.geojson.gz (Atlas country/region map). The build +# only emits dist, so these must be copied explicitly or the features silently +# degrade to empty in the image. +COPY --from=server-builder /app/server/assets ./server/assets +# tsconfig-paths/register reads this at runtime to resolve MCP SDK paths. +COPY server/tsconfig.json ./server/ +COPY --from=shared-builder /app/shared/dist ./shared/dist +COPY --from=client-builder /app/client/dist ./server/public +COPY --from=client-builder /app/client/public/fonts ./server/public/fonts + +RUN mkdir -p /app/data/logs /app/uploads/files /app/uploads/covers /app/uploads/avatars /app/uploads/photos && \ + ln -s /app/uploads /app/server/uploads && \ + ln -s /app/data /app/server/data && \ chown -R node:node /app ENV NODE_ENV=production @@ -39,4 +105,8 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ CMD wget -qO- http://localhost:3000/api/health || exit 1 ENTRYPOINT ["dumb-init", "--"] -CMD ["sh", "-c", "chown -R node:node /app/data /app/uploads 2>/dev/null || true; exec su-exec node node --import tsx src/index.ts"] +# Preflight: if the app code is missing, a volume was almost certainly mounted +# over /app (it hides the image's node_modules + dist). Fail with actionable +# guidance instead of a cryptic "Cannot find module 'tsconfig-paths/register'". +# cd into server/ so tsconfig-paths/register finds tsconfig.json and ../node_modules resolves correctly. +CMD ["sh", "-c", "if [ ! -f /app/server/dist/index.js ] || [ ! -d /app/node_modules/tsconfig-paths ]; then echo 'FATAL: TREK application files are missing from the image.'; echo 'A volume is likely mounted over /app, which hides the app code.'; echo 'Mount ONLY your data and uploads dirs: -v ./data:/app/data -v ./uploads:/app/uploads'; echo 'Do NOT mount a volume at /app. See the Troubleshooting section of the README.'; exit 1; fi; chown -R node:node /app/data /app/uploads 2>/dev/null || true; cd /app/server && exec gosu node node --require tsconfig-paths/register dist/index.js"] diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 00000000..630a78f4 --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,33 @@ +# Third-party data & attributions + +TREK bundles and uses third-party data that requires attribution. + +## geoBoundaries — country & sub-national boundaries + +The Atlas map's administrative boundaries (admin-0 countries and admin-1 +provinces/counties), shipped at `server/assets/atlas/admin0.geojson.gz` and +`server/assets/atlas/admin1.geojson.gz` and generated by +`server/scripts/build-atlas-geo.mjs`, are derived from **geoBoundaries**. + +> Runfola, D. et al. (2020) geoBoundaries: A global database of political +> administrative boundaries. PLoS ONE 15(4): e0231866. +> https://doi.org/10.1371/journal.pone.0231866 + +geoBoundaries is licensed under **CC BY 4.0** +(https://creativecommons.org/licenses/by/4.0/). Source: https://www.geoboundaries.org/ + +The bundled files are simplified (coordinate-quantized) and re-tagged with the +property names TREK consumes. Country borders (`admin0`) derive from the geoBoundaries +CGAZ composite; sub-national regions (`admin1`) derive from the per-country open +(gbOpen) release. + +## OpenStreetMap — geocoding + +Atlas reverse-geocodes places via the **Nominatim** service. Geocoding data is +© OpenStreetMap contributors, licensed under the Open Database License (ODbL). +https://www.openstreetmap.org/copyright + +## OurAirports — airport reference data + +`server/assets/airports.json` is built from **OurAirports** +(https://ourairports.com/data/), released into the public domain. diff --git a/README.md b/README.md index b1b68317..3f4d9549 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,10 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa Dashboard Trip planner with 3D map Journey journal - Budget tracker + Costs · expense splitting Atlas · visited countries Vacay planner - Iceland Ring Road + Trip planner · day plan and route Admin panel @@ -79,6 +79,7 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa - **Drag & drop planner** — organise places into day plans with reordering and cross-day moves - **Interactive map** — Leaflet or Mapbox GL with 3D buildings, terrain, photo markers, clustering, route visualization - **Place search** — Google Places (photos, ratings, hours) or OpenStreetMap (free, no API key) +- **Place import** — shared Google Maps / Naver Maps lists, plus GPX and KML/KMZ/GeoJSON map files - **Day notes** — timestamped, icon-tagged notes with drag-and-drop reordering - **Route optimisation** — auto-sort places and export to Google Maps - **Weather forecasts** — 16-day via Open-Meteo (no key) + historical climate fallback @@ -89,8 +90,8 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa #### 🧳 Travel management -- **Reservations** — flights, accommodations, restaurants with status, confirmation numbers, files -- **Budget tracking** — category-based expenses with pie chart, per-person / per-day splits, multi-currency +- **Reservations** — flights, accommodations, restaurants with status, confirmation numbers, files; import from booking confirmation emails and PDFs ([KDE Itinerary](https://invent.kde.org/pim/kitinerary)) +- **Costs** — track and split trip expenses (Splitwise-style): per-person / per-day breakdowns, settle-up, multi-currency - **Packing lists** — categories, templates, user assignment, progress tracking - **Bag tracking** — optional weight tracking with iOS-style distribution - **Document manager** — attach docs, tickets, PDFs to trips / places / reservations (≤ 50 MB each) @@ -108,6 +109,7 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa - **Invite links** — one-time or reusable links with expiry - **SSO (OIDC)** — Google, Apple, Authentik, Keycloak, or any OIDC provider - **2FA** — TOTP + backup codes +- **Passkeys** — passwordless WebAuthn login (fingerprint / face / PIN / security key), admin-toggleable - **Collab suite** — group chat, shared notes, polls, day check-ins @@ -128,13 +130,13 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa #### 🧩 Addons (admin-toggleable) - **Lists** — packing lists + to-dos with templates, member assignments, optional bag tracking -- **Budget** — expense tracker with splits, pie chart, multi-currency +- **Costs** — expense tracker with splits and settle-up (who owes whom), multi-currency - **Documents** — file attachments on trips, places, and reservations - **Collab** — chat, notes, polls, day-by-day attendance - **Vacay** — personal vacation planner with calendar, 100+ country holidays, carry-over tracking - **Atlas** — world map of visited countries, bucket list, travel stats, streak tracking, liquid-glass UI - **Journey** — magazine-style travel journal with entries, photos (Immich/Synology), maps, moods -- **Naver List Import** — one-click import from shared Naver Maps lists +- **AirTrail** — connect a self-hosted AirTrail instance to import and sync flights into reservations - **MCP** — expose TREK to AI assistants via OAuth 2.1 @@ -156,8 +158,9 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa #### ⚙️ Admin & customisation - **Dashboard views** — card grid or compact list · **Dark mode** — full theme with matching status bar -- **15 languages** — EN, DE, ES, FR, IT, NL, HU, RU, ZH, ZH-TW, PL, CS, AR (RTL), BR, ID +- **20 languages** — EN, DE, ES, FR, IT, NL, HU, RU, ZH, ZH-TW, PL, CS, AR (RTL), BR, ID, TR, JA, KO, UK, GR - **Admin panel** — users, invites, packing templates, categories, addons, API keys, backups, GitHub history +- **Notifications** — per-user preferences across email (SMTP), webhook, ntfy, and an in-app notification center - **Auto-backups** — scheduled with configurable retention · **Units** — °C/°F, 12h/24h, map tile sources, default coordinates @@ -191,9 +194,9 @@ Open `http://localhost:3000`. On first boot TREK seeds an admin account — if y
![Node.js](https://img.shields.io/badge/Node.js_22-339933?style=flat-square&logo=node.js&logoColor=white) -![Express](https://img.shields.io/badge/Express-000000?style=flat-square&logo=express&logoColor=white) +![NestJS](https://img.shields.io/badge/NestJS_11-E0234E?style=flat-square&logo=nestjs&logoColor=white) ![SQLite](https://img.shields.io/badge/SQLite-003B57?style=flat-square&logo=sqlite&logoColor=white) -![React](https://img.shields.io/badge/React_18-61DAFB?style=flat-square&logo=react&logoColor=black) +![React](https://img.shields.io/badge/React_19-61DAFB?style=flat-square&logo=react&logoColor=black) ![Vite](https://img.shields.io/badge/Vite-646CFF?style=flat-square&logo=vite&logoColor=white) ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white) ![Tailwind](https://img.shields.io/badge/Tailwind-06B6D4?style=flat-square&logo=tailwindcss&logoColor=white) @@ -202,7 +205,7 @@ Open `http://localhost:3000`. On first boot TREK seeds an admin account — if y
-Real-time sync via WebSocket (`ws`). State with Zustand. Auth via JWT + OAuth 2.1 + OIDC + TOTP MFA. Weather via Open-Meteo (no key required). Maps with Leaflet and Mapbox GL. +Real-time sync via WebSocket (`ws`). Backend on NestJS 11. State with Zustand. Auth via JWT + OAuth 2.1 + OIDC + Passkeys (WebAuthn) + TOTP MFA. Weather via Open-Meteo (no key required). Maps with Leaflet and Mapbox GL.
@@ -263,7 +266,7 @@ Then: docker compose up -d ``` -**HTTPS notes:** `FORCE_HTTPS=true` is optional — it adds a 301 redirect, HSTS, CSP upgrade-insecure-requests, and forces the `secure` cookie flag. Only use it behind a TLS-terminating reverse proxy. `TRUST_PROXY=1` tells Express how many proxies sit in front so real client IPs and `X-Forwarded-Proto` work. +**HTTPS notes:** `FORCE_HTTPS=true` is optional — it adds a 301 redirect, HSTS, CSP upgrade-insecure-requests, and forces the `secure` cookie flag. Only use it behind a TLS-terminating reverse proxy. `TRUST_PROXY=1` tells the server how many proxies sit in front so real client IPs and `X-Forwarded-Proto` work. @@ -311,6 +314,9 @@ docker run -d --name trek -p 3000:3000 -v ./data:/app/data -v ./uploads:/app/upl Your data stays in the mounted `data` and `uploads` volumes — updates never touch it. +> [!IMPORTANT] +> Mount **only** the data and uploads directories — `-v ./data:/app/data -v ./uploads:/app/uploads`. **Never mount a volume at `/app`.** Doing so hides the application code shipped in the image and the container fails to start with `Cannot find module 'tsconfig-paths/register'`. If you previously mounted `/app`, switch to the two mounts above; your data in `data/` and `uploads/` is preserved. +

Rotating the Encryption Key

If you need to rotate `ENCRYPTION_KEY` (e.g. upgrading from a version that derived encryption from `JWT_SECRET`): @@ -397,12 +403,14 @@ Caddy handles TLS and WebSockets automatically. | `ENCRYPTION_KEY` | At-rest encryption key for stored secrets (API keys, MFA, SMTP, OIDC). Recommended: generate with `openssl rand -hex 32`. If unset, falls back to `data/.jwt_secret` (existing installs) or auto-generates a key (fresh installs). | Auto | | `TZ` | Timezone for logs, reminders and cron jobs (e.g. `Europe/Berlin`) | `UTC` | | `LOG_LEVEL` | `info` = concise user actions, `debug` = verbose details | `info` | -| `DEFAULT_LANGUAGE` | Default language on the login page for users with no saved preference. Browser/OS language is auto-detected first; this is the fallback. Supported: `de`, `en`, `es`, `fr`, `hu`, `nl`, `br`, `cs`, `pl`, `ru`, `zh`, `zh-TW`, `it`, `ar` | `en` | +| `DEFAULT_LANGUAGE` | Default language on the login page for users with no saved preference. Browser/OS language is auto-detected first; this is the fallback. Supported: `de`, `en`, `es`, `fr`, `hu`, `nl`, `br`, `cs`, `pl`, `ru`, `zh`, `zh-TW`, `it`, `ar`, `id`, `tr`, `ja`, `ko`, `uk`, `gr` | `en` | | `ALLOWED_ORIGINS` | Comma-separated origins for CORS and email links | same-origin | | `FORCE_HTTPS` | Optional. When `true`: 301-redirects HTTP to HTTPS, sends HSTS, adds CSP `upgrade-insecure-requests`, forces the session cookie `secure` flag. Useful behind a TLS-terminating reverse proxy. Requires `TRUST_PROXY`. | `false` | | `HSTS_INCLUDE_SUBDOMAINS` | When `true`: adds the `includeSubDomains` directive to the HSTS header, extending HTTPS enforcement to all subdomains. Only effective when HSTS is active (`FORCE_HTTPS=true` or `NODE_ENV=production`). Leave `false` if you run other services on sibling subdomains over plain HTTP. | `false` | | `COOKIE_SECURE` | Controls the `secure` flag on the `trek_session` cookie. Auto-derived: on when `NODE_ENV=production` or `FORCE_HTTPS=true`. Escape hatch: set `false` to allow session cookies over plain HTTP. Not recommended in production. | auto | -| `TRUST_PROXY` | Number of trusted reverse proxies. Tells Express to read client IP from `X-Forwarded-For` and protocol from `X-Forwarded-Proto`. Defaults to `1` in production; off in dev unless set. | `1` | +| `SESSION_DURATION` | How long a login session stays valid when **"Remember me" is unchecked** (the default): sets the `trek_session` JWT `exp` and issues a browser-session cookie (cleared when the browser closes). Accepts `ms`-style strings: `1h`, `12h`, `7d`, `30d`, `90d`. Invalid values warn at startup and fall back to the default. | `24h` | +| `SESSION_DURATION_REMEMBER` | Session length when **"Remember me" is ticked** at login: a longer-lived JWT plus a persistent `trek_session` cookie that survives browser restarts. Same format and startup-fallback behaviour as `SESSION_DURATION`. | `30d` | +| `TRUST_PROXY` | Number of trusted reverse proxies. Tells the server to read client IP from `X-Forwarded-For` and protocol from `X-Forwarded-Proto`. Defaults to `1` in production; off in dev unless set. | `1` | | `ALLOW_INTERNAL_NETWORK` | Allow outbound requests to private/RFC-1918 IPs (e.g. Immich on your LAN). Loopback and link-local addresses remain blocked. | `false` | | `APP_URL` | Public base URL of this instance (e.g. `https://trek.example.com`). Required when OIDC is enabled; used as base for email notification links. | — | | **OIDC / SSO** | | | @@ -437,6 +445,13 @@ Caddy handles TLS and WebSockets automatically.
+## Data sources + +The Atlas map's country and sub-national (province/county) boundaries come from +[**geoBoundaries**](https://www.geoboundaries.org/) (Runfola et al., 2020), licensed +[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). See [NOTICE.md](NOTICE.md) +for full third-party attributions. + ## License TREK is [AGPL v3](LICENSE). Self-host freely for personal or internal company use. If you modify and offer TREK as a network service to third parties, your modifications must be open-sourced under the same licence. diff --git a/charts/trek/templates/configmap.yaml b/charts/trek/templates/configmap.yaml index 33efce0c..62693a5a 100644 --- a/charts/trek/templates/configmap.yaml +++ b/charts/trek/templates/configmap.yaml @@ -28,6 +28,12 @@ data: {{- if .Values.env.COOKIE_SECURE }} COOKIE_SECURE: {{ .Values.env.COOKIE_SECURE | quote }} {{- end }} + {{- if .Values.env.SESSION_DURATION }} + SESSION_DURATION: {{ .Values.env.SESSION_DURATION | quote }} + {{- end }} + {{- if .Values.env.SESSION_DURATION_REMEMBER }} + SESSION_DURATION_REMEMBER: {{ .Values.env.SESSION_DURATION_REMEMBER | quote }} + {{- end }} {{- if .Values.env.TRUST_PROXY }} TRUST_PROXY: {{ .Values.env.TRUST_PROXY | quote }} {{- end }} diff --git a/charts/trek/values.yaml b/charts/trek/values.yaml index 0f19d230..f0205423 100644 --- a/charts/trek/values.yaml +++ b/charts/trek/values.yaml @@ -34,6 +34,10 @@ env: # When "true": adds includeSubDomains to the HSTS header. Only effective when HSTS is active. Leave "false" if sibling subdomains still run over plain HTTP. # COOKIE_SECURE: "true" # Auto-derived (true in production or when FORCE_HTTPS=true). Set "false" to force cookies over plain HTTP. Not recommended for production. + # SESSION_DURATION: "24h" + # How long a login session stays valid when "Remember me" is unchecked (the default): trek_session JWT exp + a browser-session cookie. Accepts 1h, 12h, 7d, 30d, 90d. Defaults to 24h. + # SESSION_DURATION_REMEMBER: "30d" + # Session length when "Remember me" is ticked: a longer-lived JWT + persistent cookie that survives browser restarts. Same format as SESSION_DURATION. Defaults to 30d. # TRUST_PROXY: "1" # Trusted proxy hops for X-Forwarded-For/X-Forwarded-Proto. Defaults to 1 in production. Must be set for FORCE_HTTPS to work. # ALLOW_INTERNAL_NETWORK: "false" diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 00000000..11215320 --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,5 @@ +# Playwright E2E (FE7) +e2e/.tmp/ +test-results/ +playwright-report/ +playwright/.cache/ diff --git a/client/.prettierrc b/client/.prettierrc new file mode 100644 index 00000000..871f7636 --- /dev/null +++ b/client/.prettierrc @@ -0,0 +1,27 @@ +{ + "printWidth": 120, + "useTabs": false, + "tabWidth": 2, + "trailingComma": "es5", + "semi": true, + "singleQuote": true, + "bracketSpacing": true, + "arrowParens": "always", + "jsxSingleQuote": false, + "bracketSameLine": false, + "endOfLine": "lf", + "plugins": [ + "prettier-plugin-organize-imports", + "@trivago/prettier-plugin-sort-imports", + "prettier-plugin-tailwindcss" + ], + "importOrder": [ + "^[a-zA-Z]", + "^@/.*" + ], + "importOrderSeparation": true, + "importOrderParserPlugins": [ + "typescript", + "decorators-legacy" + ] +} \ No newline at end of file diff --git a/client/e2e/auth.setup.ts b/client/e2e/auth.setup.ts new file mode 100644 index 00000000..46a34229 --- /dev/null +++ b/client/e2e/auth.setup.ts @@ -0,0 +1,42 @@ +import { test as setup, expect } from '@playwright/test' + +// Relative to the config dir (client/), matching `storageState` in +// playwright.config.ts. Playwright runs from the client workspace root. +const stateFile = 'e2e/.tmp/state.json' + +// Credentials match e2e/server-launch.mjs (ADMIN_EMAIL/ADMIN_PASSWORD). The +// seeded admin is created with must_change_password=1, so the first login goes +// through the forced change-password step before reaching the dashboard. +const EMAIL = 'e2e@trek.local' +const SEED_PW = 'E2eTest12345!' +const NEW_PW = 'E2eChanged12345!' + +setup('authenticate the seeded admin (incl. forced password change)', async ({ page }) => { + await page.goto('/login') + await page.locator('input[type="email"]').fill(EMAIL) + await page.locator('input[type="password"]').fill(SEED_PW) + await page.locator('button[type="submit"]').click() + + // must_change_password=1 → the change-password step renders two password + // fields (new + confirm). Selector-agnostic of the UI language. + const pw = page.locator('input[type="password"]') + await expect(pw).toHaveCount(2) + await pw.nth(0).fill(NEW_PW) + await pw.nth(1).fill(NEW_PW) + await page.locator('button[type="submit"]').click() + + await page.waitForURL('**/dashboard', { timeout: 30_000 }) + + // Dismiss the first-run "Welcome to TREK" system-notice modal(s). It renders + // asynchronously (after the notices fetch), so wait for it before clicking. + // Dismissal is recorded server-side against this user, so clearing it here + // keeps it cleared for every authenticated flow in the run (shared test DB). + const ok = page.getByRole('button', { name: 'OK', exact: true }) + await ok.waitFor({ state: 'visible', timeout: 10_000 }).catch(() => {}) + for (let i = 0; i < 8 && (await ok.isVisible().catch(() => false)); i++) { + await ok.click() + await page.waitForTimeout(400) + } + + await page.context().storageState({ path: stateFile }) +}) diff --git a/client/e2e/create-trip.spec.ts b/client/e2e/create-trip.spec.ts new file mode 100644 index 00000000..6c264c92 --- /dev/null +++ b/client/e2e/create-trip.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from '@playwright/test' + +// Trip lifecycle (core): from the dashboard, open the new-trip modal, name the +// trip, submit, and confirm it shows up on the dashboard. Exercises the whole +// authenticated stack — dashboard → TripFormModal → POST /api/trips → store → +// re-render — against the real backend + isolated test DB. +test('create a trip and see it on the dashboard', async ({ page }) => { + await page.goto('/dashboard') + + // The "+ New Trip" card is always rendered in the default (planned) filter. + await page.locator('.add-trip-card').click() + + // Scope to the shared Modal (.modal-backdrop). Its form has no in-form submit + // button (the primary action lives in the footer), so click it explicitly + // rather than pressing Enter. The Create button is the slate primary button; + // Cancel is the bordered one. + const modal = page.locator('.modal-backdrop') + await expect(modal).toBeVisible() + + const title = `E2E Trip ${Date.now()}` + await modal.locator('input[type="text"]').first().fill(title) + await modal.getByRole('button', { name: 'Create New Trip' }).click() + + await expect(page.getByText(title).first()).toBeVisible({ timeout: 15_000 }) +}) diff --git a/client/e2e/dashboard.spec.ts b/client/e2e/dashboard.spec.ts new file mode 100644 index 00000000..d906e34a --- /dev/null +++ b/client/e2e/dashboard.spec.ts @@ -0,0 +1,10 @@ +import { test, expect } from '@playwright/test' + +// Authenticated smoke: the stored session lands on the dashboard and the +// app chrome (navbar) renders instead of bouncing back to /login. +test('authenticated session reaches the dashboard', async ({ page }) => { + await page.goto('/dashboard') + await expect(page).toHaveURL(/\/dashboard/) + // The shared Navbar shows the TREK brand once authenticated. + await expect(page.getByRole('img', { name: 'TREK' }).first()).toBeVisible() +}) diff --git a/client/e2e/login.public.spec.ts b/client/e2e/login.public.spec.ts new file mode 100644 index 00000000..36fc67d3 --- /dev/null +++ b/client/e2e/login.public.spec.ts @@ -0,0 +1,8 @@ +import { test, expect } from '@playwright/test' + +// Infra smoke + first unauthenticated flow: the app boots, the backend is +// reachable through the Vite proxy, and the login screen renders its form. +test('login screen renders with a password field', async ({ page }) => { + await page.goto('/login') + await expect(page.locator('input[type="password"]')).toBeVisible() +}) diff --git a/client/e2e/server-launch.mjs b/client/e2e/server-launch.mjs new file mode 100644 index 00000000..c9cd1067 --- /dev/null +++ b/client/e2e/server-launch.mjs @@ -0,0 +1,43 @@ +// Boots the TREK backend for the Playwright E2E run against a fresh, isolated +// SQLite database. The DB file is deleted first so every run starts clean, then +// the server's own startup seeds a known admin from ADMIN_EMAIL/ADMIN_PASSWORD. +// +// The server is built once and launched as a SINGLE node process (not the +// watch-mode `npm run dev`, which spawns tsc -w + node --watch grandchildren +// that survive Playwright's teardown and then linger on :3001 with stale DB +// state). A single child is killed cleanly when Playwright tears the run down. +import { rmSync } from 'node:fs' +import { spawn, execSync } from 'node:child_process' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const here = path.dirname(fileURLToPath(import.meta.url)) +const dbFile = path.join(here, '.tmp', 'e2e.db') +const serverDir = path.join(here, '..', '..', 'server') + +for (const f of [dbFile, `${dbFile}-wal`, `${dbFile}-shm`]) { + try { rmSync(f, { force: true }) } catch {} +} + +// Build once (no watcher) — the resulting process is a single killable node. +execSync('node scripts/build.mjs', { cwd: serverDir, stdio: 'inherit' }) + +const env = { + ...process.env, + TREK_DB_FILE: dbFile, + ADMIN_EMAIL: 'e2e@trek.local', + ADMIN_PASSWORD: 'E2eTest12345!', + PORT: '3001', + NODE_ENV: 'development', +} + +const child = spawn(process.execPath, ['--require', 'tsconfig-paths/register', 'dist/index.js'], { + cwd: serverDir, + env, + stdio: 'inherit', +}) +const stop = () => { try { child.kill() } catch {} } +process.on('SIGINT', stop) +process.on('SIGTERM', stop) +process.on('exit', stop) +child.on('exit', code => process.exit(code ?? 0)) diff --git a/client/e2e/trip-planner.spec.ts b/client/e2e/trip-planner.spec.ts new file mode 100644 index 00000000..9a4b8b2c --- /dev/null +++ b/client/e2e/trip-planner.spec.ts @@ -0,0 +1,23 @@ +import { test, expect } from '@playwright/test' + +// Open a trip into the planner: create a trip, open it from the dashboard, and +// confirm the trip planner (TripPlannerPage — the app's largest page) actually +// mounts, proving the day-plan/map shell renders rather than crashing on load. +test('open a trip and land in the planner with a map', async ({ page }) => { + await page.goto('/dashboard') + + // Create a trip to open. + await page.locator('.add-trip-card').click() + const modal = page.locator('.modal-backdrop') + await expect(modal).toBeVisible() + const title = `E2E Planner ${Date.now()}` + await modal.locator('input[type="text"]').first().fill(title) + await modal.getByRole('button', { name: 'Create New Trip' }).click() + + // Open it from the dashboard. + await page.getByText(title).first().click() + + await expect(page).toHaveURL(/\/trips\/\d+/) + // The planner shows a Leaflet map once mounted (past the splash screen). + await expect(page.locator('.leaflet-container')).toBeVisible({ timeout: 20_000 }) +}) diff --git a/client/eslint.config.mjs b/client/eslint.config.mjs new file mode 100644 index 00000000..6e48d7e4 --- /dev/null +++ b/client/eslint.config.mjs @@ -0,0 +1,78 @@ +import js from '@eslint/js'; + +import gitignore from 'eslint-config-flat-gitignore'; +import eslintConfigPrettier from 'eslint-config-prettier'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; + +// Minimal stub so the existing `// eslint-disable-next-line react/no-danger` +// directive in src/i18n/TransHtml.tsx resolves without pulling in the full +// eslint-plugin-react (not a dependency here). The rule is a no-op. +const reactStub = { + rules: { + 'no-danger': { + meta: { schema: [] }, + create() { + return {}; + }, + }, + }, +}; + +export default tseslint.config( + gitignore({ strict: false }), + { + ignores: [ + 'node_modules', + 'dist', + 'coverage', + 'public', + 'test-results', + 'playwright-report', + 'e2e/**', + 'scripts/**', + '**/*.config.js', + '**/*.config.ts', + '**/*.config.mjs', + ], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + eslintConfigPrettier, + { + files: ['src/**/*.{ts,tsx}', 'tests/**/*.{ts,tsx}'], + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + react: reactStub, + }, + rules: { + 'react/no-danger': 'off', + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + + // --- Severities tuned to keep CI green on a codebase that was never linted --- + // (each rule below has pre-existing violations; surfaced as warnings, not blockers) + + // rules-of-hooks has one conditional-hook violation in PlaceInspector.tsx -> warn (not error). + 'react-hooks/rules-of-hooks': 'warn', + 'react-hooks/exhaustive-deps': 'warn', + + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }, + ], + '@typescript-eslint/no-unused-expressions': 'warn', + '@typescript-eslint/no-unsafe-function-type': 'warn', + '@typescript-eslint/no-this-alias': 'warn', + '@typescript-eslint/no-non-null-asserted-optional-chain': 'warn', + + // js.recommended rules with pre-existing hits. + 'no-empty': 'warn', + 'no-useless-escape': 'warn', + 'no-useless-assignment': 'warn', + 'preserve-caught-error': 'warn', + }, + }, +); diff --git a/client/package-lock.json b/client/package-lock.json deleted file mode 100644 index 62dd3f9e..00000000 --- a/client/package-lock.json +++ /dev/null @@ -1,11086 +0,0 @@ -{ - "name": "trek-client", - "version": "3.0.22", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "trek-client", - "version": "3.0.22", - "dependencies": { - "@react-pdf/renderer": "^4.3.2", - "axios": "^1.6.7", - "dexie": "^4.4.2", - "heic-to": "^1.4.2", - "leaflet": "^1.9.4", - "lucide-react": "^0.344.0", - "mapbox-gl": "^3.22.0", - "marked": "^18.0.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-dropzone": "^14.4.1", - "react-leaflet": "^4.2.1", - "react-leaflet-cluster": "^2.1.0", - "react-markdown": "^10.1.0", - "react-router-dom": "^6.22.2", - "react-window": "^2.2.7", - "rehype-sanitize": "^6.0.0", - "remark-breaks": "^4.0.0", - "remark-gfm": "^4.0.1", - "topojson-client": "^3.1.0", - "zustand": "^4.5.2" - }, - "devDependencies": { - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.2", - "@testing-library/user-event": "^14.6.1", - "@types/leaflet": "^1.9.8", - "@types/react": "^18.2.61", - "@types/react-dom": "^18.2.19", - "@types/react-window": "^1.8.8", - "@vitejs/plugin-react": "^4.2.1", - "@vitest/coverage-v8": "^3.2.4", - "autoprefixer": "^10.4.18", - "fake-indexeddb": "^6.2.5", - "jsdom": "^29.0.1", - "msw": "^2.13.0", - "postcss": "^8.4.35", - "sharp": "^0.33.0", - "tailwindcss": "^3.4.1", - "typescript": "^6.0.2", - "vite": "^5.1.4", - "vite-plugin-pwa": "^0.21.0", - "vitest": "^3.2.4" - } - }, - "node_modules/@adobe/css-tools": { - "version": "4.4.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@apideck/better-ajv-errors": { - "version": "0.3.7", - "dev": true, - "license": "MIT", - "dependencies": { - "jsonpointer": "^5.0.1", - "leven": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "ajv": ">=8" - } - }, - "node_modules/@asamuzakjp/css-color": { - "version": "5.1.10", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^3.1.1", - "@csstools/css-color-parser": "^4.0.2", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "7.0.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@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.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@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-annotate-as-pure": { - "version": "7.27.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.6", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.28.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "regexpu-core": "^6.3.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "debug": "^4.4.3", - "lodash.debounce": "^4.0.8", - "resolve": "^1.22.11" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.28.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.29.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/template": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.29.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz", - "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.29.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.29.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.29.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.28.6", - "@babel/plugin-syntax-import-attributes": "^7.28.6", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.29.0", - "@babel/plugin-transform-async-to-generator": "^7.28.6", - "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.6", - "@babel/plugin-transform-class-properties": "^7.28.6", - "@babel/plugin-transform-class-static-block": "^7.28.6", - "@babel/plugin-transform-classes": "^7.28.6", - "@babel/plugin-transform-computed-properties": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-dotall-regex": "^7.28.6", - "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", - "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.6", - "@babel/plugin-transform-exponentiation-operator": "^7.28.6", - "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/plugin-transform-for-of": "^7.27.1", - "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.28.6", - "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", - "@babel/plugin-transform-member-expression-literals": "^7.27.1", - "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.28.6", - "@babel/plugin-transform-modules-systemjs": "^7.29.0", - "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", - "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", - "@babel/plugin-transform-numeric-separator": "^7.28.6", - "@babel/plugin-transform-object-rest-spread": "^7.28.6", - "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.28.6", - "@babel/plugin-transform-optional-chaining": "^7.28.6", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.28.6", - "@babel/plugin-transform-private-property-in-object": "^7.28.6", - "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.29.0", - "@babel/plugin-transform-regexp-modifiers": "^7.28.6", - "@babel/plugin-transform-reserved-words": "^7.27.1", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.28.6", - "@babel/plugin-transform-sticky-regex": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-typeof-symbol": "^7.27.1", - "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.28.6", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.15", - "babel-plugin-polyfill-corejs3": "^0.14.0", - "babel-plugin-polyfill-regenerator": "^0.6.6", - "core-js-compat": "^3.48.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@bramus/specificity": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "^3.0.0" - }, - "bin": { - "specificity": "bin/cli.js" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "6.0.2", - "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.2.0", - "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.1.0", - "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.0.2", - "@csstools/css-calc": "^3.2.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", - "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.3", - "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", - "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/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@exodus/bytes": { - "version": "1.15.0", - "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/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.2.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@inquirer/ansi": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/confirm": { - "version": "5.1.21", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core": { - "version": "10.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/figures": { - "version": "1.0.15", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/type": { - "version": "3.0.10", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "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", - "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", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mapbox/mapbox-gl-supported": { - "version": "3.0.0", - "license": "BSD-3-Clause" - }, - "node_modules/@mapbox/point-geometry": { - "version": "1.1.0", - "license": "ISC" - }, - "node_modules/@mapbox/tiny-sdf": { - "version": "2.1.0", - "license": "BSD-2-Clause" - }, - "node_modules/@mapbox/unitbezier": { - "version": "0.0.1", - "license": "BSD-2-Clause" - }, - "node_modules/@mapbox/vector-tile": { - "version": "2.0.4", - "license": "BSD-3-Clause", - "dependencies": { - "@mapbox/point-geometry": "~1.1.0", - "@types/geojson": "^7946.0.16", - "pbf": "^4.0.1" - } - }, - "node_modules/@mswjs/interceptors": { - "version": "0.41.3", - "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/@noble/ciphers": { - "version": "1.3.0", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@open-draft/deferred-promise": { - "version": "2.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@open-draft/logger": { - "version": "0.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-node-process": "^1.2.0", - "outvariant": "^1.4.0" - } - }, - "node_modules/@open-draft/until": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@react-leaflet/core": { - "version": "2.1.0", - "license": "Hippocratic-2.1", - "peerDependencies": { - "leaflet": "^1.9.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@react-pdf/fns": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/@react-pdf/font": { - "version": "4.0.6", - "license": "MIT", - "dependencies": { - "@react-pdf/pdfkit": "^5.0.0", - "@react-pdf/types": "^2.10.0", - "fontkit": "^2.0.2", - "is-url": "^1.2.4" - } - }, - "node_modules/@react-pdf/image": { - "version": "3.0.4", - "license": "MIT", - "dependencies": { - "@react-pdf/png-js": "^3.0.0", - "jay-peg": "^1.1.1" - } - }, - "node_modules/@react-pdf/layout": { - "version": "4.5.1", - "license": "MIT", - "dependencies": { - "@react-pdf/fns": "3.1.3", - "@react-pdf/image": "^3.0.4", - "@react-pdf/primitives": "^4.2.0", - "@react-pdf/stylesheet": "^6.1.4", - "@react-pdf/textkit": "^6.2.0", - "@react-pdf/types": "^2.10.0", - "emoji-regex-xs": "^1.0.0", - "queue": "^6.0.1", - "yoga-layout": "^3.2.1" - } - }, - "node_modules/@react-pdf/pdfkit": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.20.13", - "@noble/ciphers": "^1.0.0", - "@noble/hashes": "^1.6.0", - "@react-pdf/png-js": "^3.0.0", - "browserify-zlib": "^0.2.0", - "fontkit": "^2.0.2", - "jay-peg": "^1.1.1", - "js-md5": "^0.8.3", - "linebreak": "^1.1.0", - "vite-compatible-readable-stream": "^3.6.1" - } - }, - "node_modules/@react-pdf/png-js": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "browserify-zlib": "^0.2.0" - } - }, - "node_modules/@react-pdf/primitives": { - "version": "4.2.0", - "license": "MIT" - }, - "node_modules/@react-pdf/reconciler": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "object-assign": "^4.1.1", - "scheduler": "0.25.0-rc-603e6108-20241029" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@react-pdf/render": { - "version": "4.4.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.20.13", - "@react-pdf/fns": "3.1.3", - "@react-pdf/primitives": "^4.2.0", - "@react-pdf/textkit": "^6.2.0", - "@react-pdf/types": "^2.10.0", - "abs-svg-path": "^0.1.1", - "color-string": "^1.9.1", - "normalize-svg-path": "^1.1.0", - "parse-svg-path": "^0.1.2", - "svg-arc-to-cubic-bezier": "^3.2.0" - } - }, - "node_modules/@react-pdf/renderer": { - "version": "4.4.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.20.13", - "@react-pdf/fns": "3.1.3", - "@react-pdf/font": "^4.0.6", - "@react-pdf/layout": "^4.5.1", - "@react-pdf/pdfkit": "^5.0.0", - "@react-pdf/primitives": "^4.2.0", - "@react-pdf/reconciler": "^2.0.0", - "@react-pdf/render": "^4.4.1", - "@react-pdf/types": "^2.10.0", - "events": "^3.3.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "queue": "^6.0.1" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@react-pdf/stylesheet": { - "version": "6.1.4", - "license": "MIT", - "dependencies": { - "@react-pdf/fns": "3.1.3", - "@react-pdf/types": "^2.10.0", - "color-string": "^1.9.1", - "hsl-to-hex": "^1.0.0", - "media-engine": "^1.0.3", - "postcss-value-parser": "^4.1.0" - } - }, - "node_modules/@react-pdf/textkit": { - "version": "6.2.0", - "license": "MIT", - "dependencies": { - "@react-pdf/fns": "3.1.3", - "bidi-js": "^1.0.2", - "hyphen": "^1.6.4", - "unicode-properties": "^1.4.1" - } - }, - "node_modules/@react-pdf/types": { - "version": "2.10.0", - "license": "MIT", - "dependencies": { - "@react-pdf/font": "^4.0.6", - "@react-pdf/primitives": "^4.2.0", - "@react-pdf/stylesheet": "^6.1.4" - } - }, - "node_modules/@remix-run/router": { - "version": "1.23.2", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/plugin-babel": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.1.0.tgz", - "integrity": "sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@rollup/pluginutils": "^5.0.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "@types/babel__core": "^7.1.9", - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "@types/babel__core": { - "optional": true - }, - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", - "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-replace": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz", - "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "magic-string": "^0.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-terser": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", - "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "serialize-javascript": "^7.0.3", - "smob": "^1.0.0", - "terser": "^5.17.4" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", - "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", - "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", - "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", - "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", - "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", - "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", - "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", - "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", - "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", - "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", - "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", - "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", - "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", - "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", - "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", - "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", - "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.1", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", - "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", - "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", - "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", - "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", - "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", - "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", - "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@swc/helpers": { - "version": "0.5.21", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "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/jest-dom": { - "version": "6.9.1", - "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": ">=14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "dev": true, - "license": "MIT" - }, - "node_modules/@testing-library/react": { - "version": "16.3.2", - "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/user-event": { - "version": "14.6.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@trickfilm400/rollup-plugin-off-main-thread": { - "version": "3.0.0-pre1", - "resolved": "https://registry.npmjs.org/@trickfilm400/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-3.0.0-pre1.tgz", - "integrity": "sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "ejs": "^3.1.10", - "json5": "^2.2.3", - "magic-string": "^0.30.21", - "string.prototype.matchall": "^4.0.12" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "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", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/debug": { - "version": "4.1.13", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "license": "MIT" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "license": "MIT" - }, - "node_modules/@types/geojson-vt": { - "version": "3.2.5", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/leaflet": { - "version": "1.9.21", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "license": "MIT" - }, - "node_modules/@types/pbf": { - "version": "3.0.5", - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.28", - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@types/react-window": { - "version": "1.8.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/statuses": { - "version": "2.0.6", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/supercluster": { - "version": "7.1.3", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "license": "MIT" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "license": "ISC" - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@vitest/coverage-v8": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^1.0.2", - "ast-v8-to-istanbul": "^0.3.3", - "debug": "^4.4.1", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.17", - "magicast": "^0.3.5", - "std-env": "^3.9.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "3.2.4", - "vitest": "3.2.4" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/expect": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/abs-svg-path": { - "version": "0.1.1", - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.16.0", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ajv": { - "version": "8.18.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/aria-query": { - "version": "5.3.0", - "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/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", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/ast-v8-to-istanbul": { - "version": "0.3.12", - "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", - "dev": true, - "license": "MIT" - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "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/asynckit": { - "version": "0.4.0", - "license": "MIT" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/attr-accept": { - "version": "2.2.5", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/autoprefixer": { - "version": "10.5.0", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.2", - "caniuse-lite": "^1.0.30001787", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "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/axios": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", - "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", - "proxy-from-env": "^2.1.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.17", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-define-polyfill-provider": "^0.6.8", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.14.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.8", - "core-js-compat": "^3.48.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.8" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "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/base64-js": { - "version": "1.5.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.19", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/bidi-js": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/brotli": { - "version": "1.3.3", - "license": "MIT", - "dependencies": { - "base64-js": "^1.1.2" - } - }, - "node_modules/browserify-zlib": { - "version": "0.2.0", - "license": "MIT", - "dependencies": { - "pako": "~1.0.5" - } - }, - "node_modules/browserslist": { - "version": "4.28.2", - "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.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/cac": { - "version": "6.7.14", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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", - "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/camelcase-css": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001788", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/ccount": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chai": { - "version": "5.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/cheap-ruler": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/check-error": { - "version": "2.1.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/cli-width": { - "version": "4.1.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "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/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "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/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "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/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "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/clone": { - "version": "2.1.2", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/color": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/common-tags": { - "version": "1.8.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/core-js-compat": { - "version": "3.49.0", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypto-random-string": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/css-tree": { - "version": "3.2.1", - "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", - "dev": true, - "license": "MIT" - }, - "node_modules/csscolorparser": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "license": "MIT" - }, - "node_modules/data-urls": { - "version": "7.0.0", - "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-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", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "dev": true, - "license": "MIT" - }, - "node_modules/decode-named-character-reference": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "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/delayed-stream": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/devlop": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/dexie": { - "version": "4.4.2", - "license": "Apache-2.0" - }, - "node_modules/dfa": { - "version": "1.2.0", - "license": "MIT" - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dlv": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "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/earcut": { - "version": "3.0.2", - "license": "ISC" - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.336", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex-xs": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/entities": { - "version": "6.0.1", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "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-define-property": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "dev": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "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-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.21.5", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eta": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eta/-/eta-4.6.0.tgz", - "integrity": "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/bgub/eta?sponsor=1" - } - }, - "node_modules/events": { - "version": "3.3.0", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "license": "MIT" - }, - "node_modules/fake-indexeddb": { - "version": "6.2.5", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.20.1", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-selector": { - "version": "2.1.2", - "license": "MIT", - "dependencies": { - "tslib": "^2.7.0" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/filelist": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", - "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/fontkit": { - "version": "2.0.4", - "license": "MIT", - "dependencies": { - "@swc/helpers": "^0.5.12", - "brotli": "^1.3.2", - "clone": "^2.1.2", - "dfa": "^1.2.0", - "fast-deep-equal": "^3.1.3", - "restructure": "^3.0.0", - "tiny-inflate": "^1.0.3", - "unicode-properties": "^1.4.0", - "unicode-trie": "^2.0.0" - } - }, - "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/foreground-child": { - "version": "3.3.1", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fs-extra": { - "version": "9.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "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", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "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", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/geojson-vt": { - "version": "4.0.2", - "license": "ISC" - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "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-own-enumerable-property-symbols": { - "version": "3.0.2", - "dev": true, - "license": "ISC" - }, - "node_modules/get-proto": { - "version": "1.0.1", - "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/gl-matrix": { - "version": "3.4.4", - "license": "MIT" - }, - "node_modules/glob": { - "version": "10.5.0", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "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", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "dev": true, - "license": "ISC" - }, - "node_modules/graphql": { - "version": "16.13.2", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/grid-index": { - "version": "1.1.0", - "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", - "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", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "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.2", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hast-util-sanitize": { - "version": "5.0.2", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "unist-util-position": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.6", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/headers-polyfill": { - "version": "4.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/heic-to": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/heic-to/-/heic-to-1.4.2.tgz", - "integrity": "sha512-y69thwxfNcEm2Vk8lbOD/cMabnvMJyOREfJYiCHcXCDqlfcPyJoBhyRc8+iDe1B95LRfpbTOpzxzY1xbRkdwBA==", - "license": "LGPL-3.0" - }, - "node_modules/hsl-to-hex": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "hsl-to-rgb-for-reals": "^1.1.0" - } - }, - "node_modules/hsl-to-rgb-for-reals": { - "version": "1.1.1", - "license": "ISC" - }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.6.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/html-url-attributes": { - "version": "3.0.1", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hyphen": { - "version": "1.14.1", - "license": "ISC" - }, - "node_modules/idb": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", - "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/indent-string": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "license": "ISC" - }, - "node_modules/inline-style-parser": { - "version": "0.2.7", - "license": "MIT" - }, - "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/is-alphabetical": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "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-arrayish": { - "version": "0.3.4", - "license": "MIT" - }, - "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-binary-path": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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-core-module": { - "version": "2.16.1", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "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-decimal": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "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", - "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", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "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-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true, - "license": "MIT" - }, - "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", - "dev": true, - "license": "MIT" - }, - "node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "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-obj": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "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-regexp": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "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-stream": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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-url": { - "version": "1.2.4", - "license": "MIT" - }, - "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/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", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jay-peg": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "restructure": "^3.0.0" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-md5": { - "version": "0.8.3", - "license": "MIT" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "license": "MIT" - }, - "node_modules/jsdom": { - "version": "29.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^5.1.5", - "@asamuzakjp/dom-selector": "^7.0.6", - "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.1", - "@exodus/bytes": "^1.15.0", - "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.2.7", - "parse5": "^8.0.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.1", - "undici": "^7.24.5", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.1", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.3.5", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonpointer": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kdbush": { - "version": "4.0.2", - "license": "ISC" - }, - "node_modules/leaflet": { - "version": "1.9.4", - "license": "BSD-2-Clause" - }, - "node_modules/leaflet.markercluster": { - "version": "1.5.3", - "license": "MIT", - "peerDependencies": { - "leaflet": "^1.3.1" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/linebreak": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "base64-js": "0.0.8", - "unicode-trie": "^2.0.0" - } - }, - "node_modules/linebreak/node_modules/base64-js": { - "version": "0.0.8", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "dev": true, - "license": "MIT" - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/loupe": { - "version": "3.2.1", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.344.0", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/magicast": { - "version": "0.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.7.4", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mapbox-gl": { - "version": "3.22.0", - "license": "SEE LICENSE IN LICENSE.txt", - "workspaces": [ - "src/style-spec", - "packages/pmtiles-provider", - "test/build/vite", - "test/build/webpack", - "test/build/typings" - ], - "dependencies": { - "@mapbox/mapbox-gl-supported": "^3.0.0", - "@mapbox/point-geometry": "^1.1.0", - "@mapbox/tiny-sdf": "^2.0.6", - "@mapbox/unitbezier": "^0.0.1", - "@mapbox/vector-tile": "^2.0.4", - "@types/geojson": "^7946.0.16", - "@types/geojson-vt": "^3.2.5", - "@types/pbf": "^3.0.5", - "@types/supercluster": "^7.1.3", - "cheap-ruler": "^4.0.0", - "csscolorparser": "~1.0.3", - "earcut": "^3.0.1", - "geojson-vt": "^4.0.2", - "gl-matrix": "^3.4.4", - "grid-index": "^1.1.0", - "kdbush": "^4.0.2", - "martinez-polygon-clipping": "^0.8.1", - "murmurhash-js": "^1.0.0", - "pbf": "^4.0.1", - "potpack": "^2.0.0", - "quickselect": "^3.0.0", - "supercluster": "^8.0.1", - "tinyqueue": "^3.0.0" - } - }, - "node_modules/markdown-table": { - "version": "3.0.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/marked": { - "version": "18.0.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.3.tgz", - "integrity": "sha512-7VT90JOkDeaRWpfjOReRGPEKn0ecdARBkDGL+tT1wZY0efPPqkUxLUSmzy/C7TIylQYJC9STISEsCHrqb/7VIA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/martinez-polygon-clipping": { - "version": "0.8.1", - "license": "MIT", - "dependencies": { - "robust-predicates": "^2.0.4", - "splaytree": "^0.1.4", - "tinyqueue": "3.0.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.3", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-gfm-autolink-literal": "^2.0.0", - "mdast-util-gfm-footnote": "^2.0.0", - "mdast-util-gfm-strikethrough": "^2.0.0", - "mdast-util-gfm-table": "^2.0.0", - "mdast-util-gfm-task-list-item": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "micromark-util-character": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-newline-to-break": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-find-and-replace": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.1", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdn-data": { - "version": "2.27.1", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/media-engine": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromark": { - "version": "4.0.2", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^2.0.0", - "micromark-extension-gfm-footnote": "^2.0.0", - "micromark-extension-gfm-strikethrough": "^2.0.0", - "micromark-extension-gfm-table": "^2.0.0", - "micromark-extension-gfm-tagfilter": "^2.0.0", - "micromark-extension-gfm-task-list-item": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimatch/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/minimatch/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "license": "MIT" - }, - "node_modules/msw": { - "version": "2.13.3", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@inquirer/confirm": "^5.0.0", - "@mswjs/interceptors": "^0.41.2", - "@open-draft/deferred-promise": "^2.2.0", - "@types/statuses": "^2.0.6", - "cookie": "^1.0.2", - "graphql": "^16.12.0", - "headers-polyfill": "^4.0.2", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "path-to-regexp": "^6.3.0", - "picocolors": "^1.1.1", - "rettime": "^0.11.7", - "statuses": "^2.0.2", - "strict-event-emitter": "^0.5.1", - "tough-cookie": "^6.0.0", - "type-fest": "^5.2.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/murmurhash-js": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/mute-stream": { - "version": "2.0.0", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/mz": { - "version": "2.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "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/node-releases": { - "version": "2.0.37", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-svg-path": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "svg-arc-to-cubic-bezier": "^3.0.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "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/outvariant": { - "version": "1.4.3", - "dev": true, - "license": "MIT" - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/pako": { - "version": "1.0.11", - "license": "(MIT AND Zlib)" - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "license": "MIT" - }, - "node_modules/parse-svg-path": { - "version": "0.1.2", - "license": "MIT" - }, - "node_modules/parse5": { - "version": "8.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "dev": true, - "license": "ISC" - }, - "node_modules/path-to-regexp": { - "version": "6.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/pathe": { - "version": "2.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/pbf": { - "version": "4.0.1", - "license": "BSD-3-Clause", - "dependencies": { - "resolve-protobuf-schema": "^2.1.0" - }, - "bin": { - "pbf": "bin/pbf" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "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.10", - "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.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "license": "MIT" - }, - "node_modules/potpack": { - "version": "2.1.0", - "license": "ISC" - }, - "node_modules/pretty-bytes": { - "version": "6.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "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/prop-types": { - "version": "15.8.1", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "license": "MIT" - }, - "node_modules/property-information": { - "version": "7.1.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/protocol-buffers-schema": { - "version": "3.6.1", - "license": "MIT" - }, - "node_modules/proxy-from-env": { - "version": "2.1.0", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/quickselect": { - "version": "3.0.0", - "license": "ISC" - }, - "node_modules/react": { - "version": "18.3.1", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-dom/node_modules/scheduler": { - "version": "0.23.2", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/react-dropzone": { - "version": "14.4.1", - "license": "MIT", - "dependencies": { - "attr-accept": "^2.2.4", - "file-selector": "^2.1.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">= 10.13" - }, - "peerDependencies": { - "react": ">= 16.8 || 18.0.0" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/react-leaflet": { - "version": "4.2.1", - "license": "Hippocratic-2.1", - "dependencies": { - "@react-leaflet/core": "^2.1.0" - }, - "peerDependencies": { - "leaflet": "^1.9.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/react-leaflet-cluster": { - "version": "2.1.0", - "license": "SEE LICENSE IN ", - "dependencies": { - "leaflet.markercluster": "^1.5.3" - }, - "peerDependencies": { - "leaflet": "^1.8.0", - "react": "^18.0.0", - "react-dom": "^18.0.0", - "react-leaflet": "^4.0.0" - } - }, - "node_modules/react-markdown": { - "version": "10.1.0", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "html-url-attributes": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "unified": "^11.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=18", - "react": ">=18" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "6.30.3", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.30.3", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.2", - "react-router": "6.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/react-window": { - "version": "2.2.7", - "license": "MIT", - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/redent": { - "version": "3.0.0", - "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/regenerate": { - "version": "1.4.2", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "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/regexpu-core": { - "version": "6.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.2", - "regjsgen": "^0.8.0", - "regjsparser": "^0.13.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.2.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsgen": { - "version": "0.8.0", - "dev": true, - "license": "MIT" - }, - "node_modules/regjsparser": { - "version": "0.13.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~3.1.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/rehype-sanitize": { - "version": "6.0.0", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-sanitize": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-breaks": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-newline-to-break": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-gfm": { - "version": "4.0.1", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-gfm": "^3.0.0", - "micromark-extension-gfm": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.2", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-stringify": { - "version": "11.0.0", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.12", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-protobuf-schema": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "protocol-buffers-schema": "^3.3.1" - } - }, - "node_modules/restructure": { - "version": "3.0.2", - "license": "MIT" - }, - "node_modules/rettime": { - "version": "0.11.7", - "dev": true, - "license": "MIT" - }, - "node_modules/reusify": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/robust-predicates": { - "version": "2.0.4", - "license": "Unlicense" - }, - "node_modules/rollup": { - "version": "4.60.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.1", - "@rollup/rollup-android-arm64": "4.60.1", - "@rollup/rollup-darwin-arm64": "4.60.1", - "@rollup/rollup-darwin-x64": "4.60.1", - "@rollup/rollup-freebsd-arm64": "4.60.1", - "@rollup/rollup-freebsd-x64": "4.60.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", - "@rollup/rollup-linux-arm-musleabihf": "4.60.1", - "@rollup/rollup-linux-arm64-gnu": "4.60.1", - "@rollup/rollup-linux-arm64-musl": "4.60.1", - "@rollup/rollup-linux-loong64-gnu": "4.60.1", - "@rollup/rollup-linux-loong64-musl": "4.60.1", - "@rollup/rollup-linux-ppc64-gnu": "4.60.1", - "@rollup/rollup-linux-ppc64-musl": "4.60.1", - "@rollup/rollup-linux-riscv64-gnu": "4.60.1", - "@rollup/rollup-linux-riscv64-musl": "4.60.1", - "@rollup/rollup-linux-s390x-gnu": "4.60.1", - "@rollup/rollup-linux-x64-gnu": "4.60.1", - "@rollup/rollup-linux-x64-musl": "4.60.1", - "@rollup/rollup-openbsd-x64": "4.60.1", - "@rollup/rollup-openharmony-arm64": "4.60.1", - "@rollup/rollup-win32-arm64-msvc": "4.60.1", - "@rollup/rollup-win32-ia32-msvc": "4.60.1", - "@rollup/rollup-win32-x64-gnu": "4.60.1", - "@rollup/rollup-win32-x64-msvc": "4.60.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-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-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/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", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/scheduler": { - "version": "0.25.0-rc-603e6108-20241029", - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/serialize-javascript": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", - "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=20.0.0" - } - }, - "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/sharp": { - "version": "0.33.5", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" - } - }, - "node_modules/sharp/node_modules/semver": { - "version": "7.7.4", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.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", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.4", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/smob": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.1.tgz", - "integrity": "sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/source-map": { - "version": "0.8.0-beta.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "whatwg-url": "^7.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map/node_modules/tr46": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/source-map/node_modules/webidl-conversions": { - "version": "4.0.2", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/source-map/node_modules/whatwg-url": { - "version": "7.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/splaytree": { - "version": "0.1.4", - "license": "MIT" - }, - "node_modules/stackback": { - "version": "0.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "3.10.0", - "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", - "dev": true, - "license": "MIT" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "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/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "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/stringify-entities": { - "version": "4.0.4", - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/stringify-object": { - "version": "3.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "get-own-enumerable-property-symbols": "^3.0.0", - "is-obj": "^1.0.1", - "is-regexp": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "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-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/strip-comments": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-literal": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/style-to-js": { - "version": "1.1.21", - "license": "MIT", - "dependencies": { - "style-to-object": "1.0.14" - } - }, - "node_modules/style-to-object": { - "version": "1.0.14", - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.7" - } - }, - "node_modules/sucrase": { - "version": "3.35.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/supercluster": { - "version": "8.0.1", - "license": "ISC", - "dependencies": { - "kdbush": "^4.0.2" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svg-arc-to-cubic-bezier": { - "version": "3.2.0", - "license": "ISC" - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "dev": true, - "license": "MIT" - }, - "node_modules/tagged-tag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.19", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/temp-dir": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tempy": { - "version": "0.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-stream": "^2.0.0", - "temp-dir": "^2.0.0", - "type-fest": "^0.16.0", - "unique-string": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/type-fest": { - "version": "0.16.0", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terser": { - "version": "5.46.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "dev": true, - "license": "MIT" - }, - "node_modules/test-exclude": { - "version": "7.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^10.2.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tiny-inflate": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/tinybench": { - "version": "2.9.0", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "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/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinypool": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyqueue": { - "version": "3.0.0", - "license": "ISC" - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tldts": { - "version": "7.0.28", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.0.28" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.0.28", - "dev": true, - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/topojson-client": { - "version": "3.1.0", - "license": "ISC", - "dependencies": { - "commander": "2" - }, - "bin": { - "topo2geo": "bin/topo2geo", - "topomerge": "bin/topomerge", - "topoquantize": "bin/topoquantize" - } - }, - "node_modules/topojson-client/node_modules/commander": { - "version": "2.20.3", - "license": "MIT" - }, - "node_modules/tough-cookie": { - "version": "6.0.1", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tslib": { - "version": "2.8.1", - "license": "0BSD" - }, - "node_modules/type-fest": { - "version": "5.5.0", - "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.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "6.0.2", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "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/undici": { - "version": "7.25.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-properties": { - "version": "1.4.1", - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.0", - "unicode-trie": "^2.0.0" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-trie": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "pako": "^0.2.5", - "tiny-inflate": "^1.0.0" - } - }, - "node_modules/unicode-trie/node_modules/pako": { - "version": "0.2.9", - "license": "MIT" - }, - "node_modules/unified": { - "version": "11.0.5", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unique-string": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "crypto-random-string": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.1.0", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/until-async": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/kettanaito" - } - }, - "node_modules/upath": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4", - "yarn": "*" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "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/use-sync-external-store": { - "version": "1.6.0", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/vfile": { - "version": "6.0.3", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vite": { - "version": "5.4.21", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-compatible-readable-stream": { - "version": "3.6.1", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/vite-node": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-plugin-pwa": { - "version": "0.21.2", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.6", - "pretty-bytes": "^6.1.1", - "tinyglobby": "^0.2.10", - "workbox-build": "^7.3.0", - "workbox-window": "^7.3.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vite-pwa/assets-generator": "^0.2.6", - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", - "workbox-build": "^7.3.0", - "workbox-window": "^7.3.0" - }, - "peerDependenciesMeta": { - "@vite-pwa/assets-generator": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/webidl-conversions": { - "version": "8.0.1", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-mimetype": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-url": { - "version": "16.0.1", - "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/which": { - "version": "2.0.2", - "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.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "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", - "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/workbox-background-sync": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.1.tgz", - "integrity": "sha512-HhT7KE8tOWDm02wRNshXUnUPofMlhenF2DBdUnDPOubhizzPeItkYTmAB6td1Z2cjYPa98vzEiPLEuzn5hN66g==", - "dev": true, - "license": "MIT", - "dependencies": { - "idb": "^7.0.1", - "workbox-core": "7.4.1" - } - }, - "node_modules/workbox-broadcast-update": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.1.tgz", - "integrity": "sha512-uAlgslKLvbQY+suirIdnBCSYrcgBhjp81Nj4l1lj/Jmj0MJO2CJERnCJjT0GFVwmReV0N+zs78K6gqd5gr9/+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.1" - } - }, - "node_modules/workbox-build": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.1.tgz", - "integrity": "sha512-SDhxIvEAde9Gy/5w4Yo1Jh/M49Z0qE3q0oteyE8zGq0DScxFqVBcCtIXFuLtmtxRQZCMbf0prco4VyEu3KBQuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@apideck/better-ajv-errors": "^0.3.1", - "@babel/core": "^7.24.4", - "@babel/preset-env": "^7.11.0", - "@babel/runtime": "^7.11.2", - "@rollup/plugin-babel": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.3", - "@rollup/plugin-replace": "^6.0.3", - "@rollup/plugin-terser": "^1.0.0", - "@trickfilm400/rollup-plugin-off-main-thread": "^3.0.0-pre1", - "ajv": "^8.6.0", - "common-tags": "^1.8.0", - "eta": "^4.5.1", - "fast-json-stable-stringify": "^2.1.0", - "fs-extra": "^9.0.1", - "glob": "^11.0.1", - "pretty-bytes": "^5.3.0", - "rollup": "^4.53.3", - "source-map": "^0.8.0-beta.0", - "stringify-object": "^3.3.0", - "strip-comments": "^2.0.1", - "tempy": "^0.6.0", - "upath": "^1.2.0", - "workbox-background-sync": "7.4.1", - "workbox-broadcast-update": "7.4.1", - "workbox-cacheable-response": "7.4.1", - "workbox-core": "7.4.1", - "workbox-expiration": "7.4.1", - "workbox-google-analytics": "7.4.1", - "workbox-navigation-preload": "7.4.1", - "workbox-precaching": "7.4.1", - "workbox-range-requests": "7.4.1", - "workbox-recipes": "7.4.1", - "workbox-routing": "7.4.1", - "workbox-strategies": "7.4.1", - "workbox-streams": "7.4.1", - "workbox-sw": "7.4.1", - "workbox-window": "7.4.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/workbox-build/node_modules/@isaacs/cliui": { - "version": "9.0.0", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/workbox-build/node_modules/glob": { - "version": "11.1.0", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/workbox-build/node_modules/jackspeak": { - "version": "4.2.3", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^9.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/workbox-build/node_modules/lru-cache": { - "version": "11.3.5", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/workbox-build/node_modules/path-scurry": { - "version": "2.0.2", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/workbox-build/node_modules/pretty-bytes": { - "version": "5.6.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/workbox-cacheable-response": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.1.tgz", - "integrity": "sha512-8xaFoJdDc2OjrlbbL3gEeBO1WKcMwRqwLRupgqahYXu75yXajPLuwrbXMrIGZuWYXrQwk0xDjOxZ/ujCy/oJYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.1" - } - }, - "node_modules/workbox-core": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.1.tgz", - "integrity": "sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/workbox-expiration": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.1.tgz", - "integrity": "sha512-lRKUF7b+OGbeXkQk1s6MHXOa3d7Xxf7Of31W6c6hCfipfIyrtdWZ89stq21AHZMaoG7VNFoHply4Ox+rU31TWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "idb": "^7.0.1", - "workbox-core": "7.4.1" - } - }, - "node_modules/workbox-google-analytics": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.1.tgz", - "integrity": "sha512-Mks1JwLEt++ZAkF6sS1OpSh9RtAMIsiDgRpK+codiHGIPXeaUOgi4cPc3GFadUl8V5QPeypEk8Oxgl3HlwVzHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-background-sync": "7.4.1", - "workbox-core": "7.4.1", - "workbox-routing": "7.4.1", - "workbox-strategies": "7.4.1" - } - }, - "node_modules/workbox-navigation-preload": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.1.tgz", - "integrity": "sha512-C4KVsjPcYKJOhr631AxR9XoG2rLF3QiTk5aMv36MXOjtWvm8axwNFAtKUPGsWUwLXXAMgYM1En7fsvndaXeXRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.1" - } - }, - "node_modules/workbox-precaching": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.1.tgz", - "integrity": "sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.1", - "workbox-routing": "7.4.1", - "workbox-strategies": "7.4.1" - } - }, - "node_modules/workbox-range-requests": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.1.tgz", - "integrity": "sha512-7i2oxAUE82gHdAJBCAQ04JzNOdRPqzuOzGfoUyJpFSmeqBNYGPrAH8GPoPjUQTfp+NycwrD2H68VtuF8qxv0vQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.1" - } - }, - "node_modules/workbox-recipes": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.1.tgz", - "integrity": "sha512-gnbVfmV4/TtmQaM4x9AtuXhcdstJsep3XMVeztOrQVPT+R6+6DeBjGTCQ7fFCXm+4GEHUA5VEBTyi5+4gWGeog==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-cacheable-response": "7.4.1", - "workbox-core": "7.4.1", - "workbox-expiration": "7.4.1", - "workbox-precaching": "7.4.1", - "workbox-routing": "7.4.1", - "workbox-strategies": "7.4.1" - } - }, - "node_modules/workbox-routing": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.1.tgz", - "integrity": "sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.1" - } - }, - "node_modules/workbox-strategies": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.1.tgz", - "integrity": "sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.1" - } - }, - "node_modules/workbox-streams": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.1.tgz", - "integrity": "sha512-HWWtraKUbJknd9kgqGcpQ3G114HOPYvqs8HaJMDs2ebLNAimDkVDaWfAXE6Ybl+m8U6KsCE6pWyLYuigWmnAXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "workbox-core": "7.4.1", - "workbox-routing": "7.4.1" - } - }, - "node_modules/workbox-sw": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.1.tgz", - "integrity": "sha512-fez5f2DUlDJWTFYkCWQpY10N8gtztd849NswCbVFk0QlcSM4HT5A8x4g4ii650yem4I8tHY0R7JZahwp3ltIPw==", - "dev": true, - "license": "MIT" - }, - "node_modules/workbox-window": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.1.tgz", - "integrity": "sha512-notZDH2u8VXaqyuD7xaqIfEFi6SRM4SUSd7ewe9PDsVqADuepxX2ZMY3uvuZGxzY5ZOsGC/vD3A/3smFtJt4/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/trusted-types": "^2.0.2", - "workbox-core": "7.4.1" - } - }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "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/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "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/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "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/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "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/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "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/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/y18n": { - "version": "5.0.8", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "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/yargs-parser": { - "version": "21.1.1", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "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/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yoga-layout": { - "version": "3.2.1", - "license": "MIT" - }, - "node_modules/zustand": { - "version": "4.5.7", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} diff --git a/client/package.json b/client/package.json index 3d3b449d..8fc0a30e 100644 --- a/client/package.json +++ b/client/package.json @@ -1,5 +1,5 @@ { - "name": "trek-client", + "name": "@trek/client", "version": "3.0.22", "private": true, "type": "module", @@ -8,14 +8,26 @@ "prebuild": "node scripts/generate-icons.mjs", "build": "vite build", "preview": "vite preview", + "typecheck": "tsc --noEmit", "test": "vitest run", "test:unit": "vitest run tests/unit", "test:integration": "vitest run tests/integration src/**/*.test.{ts,tsx}", "test:watch": "vitest", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "lint": "eslint .", + "lint:check": "eslint .", + "lint:pages": "node scripts/check-page-pattern.mjs", + "e2e": "playwright test", + "e2e:report": "playwright show-report", + "format": "prettier --write \"src/**/*.tsx\" \"src/**/*.css\"", + "format:check": "prettier --check \"src/**/*.tsx\" \"src/**/*.css\"" }, "dependencies": { - "@react-pdf/renderer": "^4.3.2", + "@fontsource/geist-sans": "^5.2.5", + "@fontsource/poppins": "^5.2.7", + "@react-pdf/renderer": "^4.5.1", + "@simplewebauthn/browser": "^13.1.2", + "@trek/shared": "*", "axios": "^1.6.7", "dexie": "^4.4.2", "heic-to": "^1.4.2", @@ -23,11 +35,11 @@ "lucide-react": "^0.344.0", "mapbox-gl": "^3.22.0", "marked": "^18.0.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", "react-dropzone": "^14.4.1", - "react-leaflet": "^4.2.1", - "react-leaflet-cluster": "^2.1.0", + "react-leaflet": "^5.0.0", + "react-leaflet-cluster": "^4.1.3", "react-markdown": "^10.1.0", "react-router-dom": "^6.22.2", "react-window": "^2.2.7", @@ -35,28 +47,42 @@ "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.1", "topojson-client": "^3.1.0", + "zod": "^4.3.6", "zustand": "^4.5.2" }, "devDependencies": { + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.60.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/leaflet": "^1.9.8", - "@types/react": "^18.2.61", - "@types/react-dom": "^18.2.19", + "@types/node": "^25.9.3", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", "@types/react-window": "^1.8.8", - "@vitejs/plugin-react": "^4.2.1", - "@vitest/coverage-v8": "^3.2.4", + "@vitejs/plugin-react": "^6.0.2", + "@vitest/coverage-v8": "^4.1.9", "autoprefixer": "^10.4.18", + "eslint": "^10.2.1", + "eslint-config-flat-gitignore": "^2.3.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", "fake-indexeddb": "^6.2.5", "jsdom": "^29.0.1", "msw": "^2.13.0", "postcss": "^8.4.35", + "prettier": "^3.8.3", + "prettier-plugin-organize-imports": "^4.3.0", + "prettier-plugin-tailwindcss": "^0.8.0", "sharp": "^0.33.0", "tailwindcss": "^3.4.1", "typescript": "^6.0.2", - "vite": "^5.1.4", - "vite-plugin-pwa": "^0.21.0", - "vitest": "^3.2.4" + "typescript-eslint": "^8.58.2", + "vite": "^8.0.16", + "vite-plugin-pwa": "^1.3.0", + "vitest": "^4.1.9" } } diff --git a/client/playwright.config.ts b/client/playwright.config.ts new file mode 100644 index 00000000..e6868a43 --- /dev/null +++ b/client/playwright.config.ts @@ -0,0 +1,57 @@ +import { defineConfig, devices } from '@playwright/test' + +/** + * E2E harness for TREK's critical user flows (FE7). + * + * Two web servers are orchestrated: the Express/Nest backend on :3001 against an + * isolated throwaway SQLite DB (e2e/server-launch.mjs sets TREK_DB_FILE + seeds a + * known admin), and the Vite dev server on :5173 which proxies /api, /uploads, + * /ws to the backend. Tests run serially against one worker so they share the + * single seeded database deterministically. + */ +export default defineConfig({ + testDir: './e2e', + fullyParallel: false, + workers: 1, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + timeout: 45_000, + expect: { timeout: 15_000 }, + reporter: [['list']], + use: { + baseURL: 'http://localhost:5173', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + // Unauthenticated flows (login, register, public share) — no stored session. + { name: 'public', testMatch: /\.public\.spec\.ts/, use: { ...devices['Desktop Chrome'] } }, + // One-time login that persists a session for the authenticated flows. + { name: 'setup', testMatch: /auth\.setup\.ts/ }, + { + name: 'app', + testMatch: /\.spec\.ts/, + testIgnore: /(\.public\.spec\.ts|auth\.setup\.ts)/, + use: { ...devices['Desktop Chrome'], storageState: 'e2e/.tmp/state.json' }, + dependencies: ['setup'], + }, + ], + webServer: [ + { + // Always start our own backend (never reuse) so the isolated test DB is + // reset + reseeded on every run, regardless of any stray dev server. + command: 'node e2e/server-launch.mjs', + port: 3001, + reuseExistingServer: false, + timeout: 180_000, + stdout: 'pipe', + stderr: 'pipe', + }, + { + command: 'npm run dev', + port: 5173, + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, + ], +}) diff --git a/client/scripts/check-page-pattern.mjs b/client/scripts/check-page-pattern.mjs new file mode 100644 index 00000000..5d7d789c --- /dev/null +++ b/client/scripts/check-page-pattern.mjs @@ -0,0 +1,44 @@ +// Guards the "Page = wiring container + data hook" convention (see +// src/pages/PATTERN.md). A *Page.tsx default-export component should wire a +// co-located use() hook into JSX — it must not own state/effects itself. +// +// We scan only the default-export component body (from `export default function` +// up to the next top-level `function` declaration or EOF), so presentational +// sub-components and helper hooks living in the same file are not flagged. +// Context hooks like useTranslation/useParams are fine; the smell is stateful +// logic — useState/useReducer/useEffect/useLayoutEffect/useMemo/useCallback/useRef. +import { readdirSync, readFileSync } from 'node:fs' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const pagesDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'pages') +const BANNED = ['useState', 'useReducer', 'useEffect', 'useLayoutEffect', 'useMemo', 'useCallback', 'useRef'] +const bannedRe = new RegExp(`\\b(${BANNED.join('|')})\\s*\\(`) + +const violations = [] +for (const file of readdirSync(pagesDir)) { + if (!file.endsWith('Page.tsx') || file.endsWith('.test.tsx')) continue + const src = readFileSync(join(pagesDir, file), 'utf8') + const lines = src.split('\n') + const start = lines.findIndex(l => /export default function/.test(l)) + if (start === -1) continue + // The page body ends at the next top-level declaration (a `function` at + // column 0) — everything after that is a sub-component or helper. + let end = lines.length + for (let i = start + 1; i < lines.length; i++) { + if (/^(function |const [A-Z]\w* = )/.test(lines[i])) { end = i; break } + } + for (let i = start; i < end; i++) { + if (bannedRe.test(lines[i])) { + violations.push(`${file}:${i + 1} ${lines[i].trim()}`) + } + } +} + +if (violations.length > 0) { + console.error('Page-pattern violations — move this state/effect logic into the page\'s use() hook:\n') + for (const v of violations) console.error(' ' + v) + console.error(`\n${violations.length} violation(s). See src/pages/PATTERN.md.`) + process.exit(1) +} +console.log('Page pattern OK — no state/effect logic in page containers.') diff --git a/client/src/App.tsx b/client/src/App.tsx index efe22501..6bdae822 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -119,7 +119,7 @@ export default function App() { } } authApi.getAppConfig().then(async (config: { demo_mode?: boolean; dev_mode?: boolean; is_prerelease?: boolean; has_maps_key?: boolean; version?: string; timezone?: string; require_mfa?: boolean; trip_reminders_enabled?: boolean; places_photos_enabled?: boolean; places_autocomplete_enabled?: boolean; places_details_enabled?: boolean; permissions?: Record }) => { - if (config?.demo_mode) setDemoMode(true) + setDemoMode(!!config?.demo_mode) if (config?.dev_mode) setDevMode(true) if (config?.is_prerelease !== undefined) setIsPrerelease(config.is_prerelease) if (config?.version) setAppVersion(config.version) diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 57c90fbb..899453c8 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -1,31 +1,112 @@ import axios, { AxiosInstance } from 'axios' +import type { z } from 'zod' +import { + weatherResultSchema, type WeatherResult, + inAppListResultSchema, type InAppListResult, + unreadCountResultSchema, type UnreadCountResult, + channelTestResultSchema, + mapsSearchResultSchema, mapsAutocompleteResultSchema, mapsPlaceDetailsResultSchema, + mapsPlacePhotoResultSchema, mapsReverseResultSchema, mapsResolveUrlResultSchema, + type NotificationRespondRequest, + type SettingUpsertRequest, type SettingsBulkRequest, + type JourneyCreateRequest, type JourneyAddTripRequest, + type JourneyReorderEntriesRequest, type JourneyProviderPhotosRequest, + type JourneyShareLinkRequest, + type RegisterRequest, type LoginRequest, type ForgotPasswordRequest, + type ResetPasswordRequest, type ChangePasswordRequest, + type MfaVerifyLoginRequest, type MfaEnableRequest, type McpTokenCreateRequest, + type TripAddMemberRequest, type AssignmentReorderRequest, + type PackingReorderRequest, type PackingCreateBagRequest, type TodoReorderRequest, + type TripCreateRequest, type TripUpdateRequest, type TripCopyRequest, + type DayCreateRequest, type DayUpdateRequest, type DayReorderRequest, + type PlaceCreateRequest, type PlaceUpdateRequest, + type ReservationCreateRequest, type ReservationUpdateRequest, + type AccommodationCreateRequest, type AccommodationUpdateRequest, + type BudgetCreateItemRequest, type BudgetUpdateItemRequest, + type PackingCreateItemRequest, type PackingUpdateItemRequest, + type TodoCreateItemRequest, type TodoUpdateItemRequest, + type AssignmentCreateRequest, type AssignmentParticipantsRequest, type AssignmentTimeRequest, + type PlaceBulkDeleteRequest, + type DayNoteCreateRequest, type DayNoteUpdateRequest, + type PackingImportRequest, type PackingBagMembersRequest, type PackingUpdateBagRequest, + type PackingCategoryAssigneesRequest, + type BudgetUpdateMembersRequest, type BudgetToggleMemberPaidRequest, type BudgetReorderCategoriesRequest, + type TodoCategoryAssigneesRequest, + type CollabNoteCreateRequest, type CollabNoteUpdateRequest, type CollabPollCreateRequest, + type CollabPollVoteRequest, type CollabMessageCreateRequest, type CollabReactionRequest, + type FileUpdateRequest, type FileLinkRequest, + type CreateTagRequest, type UpdateTagRequest, + type CreateCategoryRequest, type UpdateCategoryRequest, + type PlaceImportListRequest, + type BookingImportPreviewItem, + type BookingImportPreviewResponse, + type BookingImportConfirmResponse, +} from '@trek/shared' import { getSocketId } from './websocket' import { isReachable, probeNow } from '../sync/connectivity' -import en from '../i18n/translations/en' -import br from '../i18n/translations/br' -import de from '../i18n/translations/de' -import es from '../i18n/translations/es' -import fr from '../i18n/translations/fr' -import it from '../i18n/translations/it' -import nl from '../i18n/translations/nl' -import pl from '../i18n/translations/pl' -import cs from '../i18n/translations/cs' -import hu from '../i18n/translations/hu' -import ru from '../i18n/translations/ru' -import zh from '../i18n/translations/zh' -import zhTw from '../i18n/translations/zhTw' -import ar from '../i18n/translations/ar' -const rateLimitTranslations: Record> = { - en, br, de, es, fr, it, nl, pl, cs, hu, ru, zh, 'zh-TW': zhTw, ar, +/** + * Validate a response payload against its @trek/shared Zod schema — but only in + * dev, and never throwing. A drift between the server contract and the client's + * expected shape is surfaced as a console warning during development; in + * production (and on any mismatch) the data passes through untouched, so adding + * validation can never break a working call. This is the typed-request helper + * the FE adopts per domain as each backend module lands on @trek/shared. + */ +const API_DEV = Boolean((import.meta as { env?: { DEV?: boolean } }).env?.DEV) +export function parseInDev(schema: S, data: unknown, label: string): z.infer { + if (API_DEV) { + const result = schema.safeParse(data) + if (!result.success) { + console.warn(`[api] ${label}: response did not match the @trek/shared schema`, result.error.issues) + } + } + return data as z.infer +} + +/** + * Same dev-only drift check as parseInDev, but passes the payload straight + * through with its original inferred type instead of the schema type. Use this + * for endpoints whose existing consumers rely on the loose `r.data` type — it + * adds the development contract-drift warning without retyping the public + * surface (so it can never break a consumer that worked before). + */ +function checkInDev(schema: z.ZodTypeAny, data: T, label: string): T { + if (API_DEV) { + const result = schema.safeParse(data) + if (!result.success) { + console.warn(`[api] ${label}: response did not match the @trek/shared schema`, result.error.issues) + } + } + return data +} +const RATE_LIMIT_MESSAGES: Record = { + en: 'Too many attempts. Please try again later.', + de: 'Zu viele Versuche. Bitte versuchen Sie es später erneut.', + es: 'Demasiados intentos. Inténtelo de nuevo más tarde.', + fr: 'Trop de tentatives. Veuillez réessayer plus tard.', + hu: 'Túl sok próbálkozás. Kérjük, próbálja újra később.', + nl: 'Te veel pogingen. Probeer het later opnieuw.', + br: 'Muitas tentativas. Tente novamente mais tarde.', + cs: 'Příliš mnoho pokusů. Zkuste to prosím znovu.', + pl: 'Zbyt wiele prób. Spróbuj ponownie później.', + ru: 'Слишком много попыток. Попробуйте позже.', + zh: '尝试次数过多,请稍后再试。', + 'zh-TW': '嘗試次數過多,請稍後再試。', + it: 'Troppi tentativi. Riprova più tardi.', + tr: 'Çok fazla deneme. Lütfen daha sonra tekrar deneyin.', + ar: 'محاولات كثيرة جدًا. يرجى المحاولة لاحقًا.', + id: 'Terlalu banyak percobaan. Coba lagi nanti.', + ja: '試行回数が多すぎます。時間をおいて再度お試しください。', + ko: '시도 횟수가 너무 많습니다. 잠시 후 다시 시도해 주세요.', + uk: 'Занадто багато спроб. Спробуйте пізніше.', } function translateRateLimit(): string { - const fallback = 'Too many attempts. Please try again later.' + const fallback = RATE_LIMIT_MESSAGES['en']! try { const lang = localStorage.getItem('app_language') || 'en' - const table = rateLimitTranslations[lang] || rateLimitTranslations.en - return (table['common.tooManyAttempts'] as string) || (rateLimitTranslations.en['common.tooManyAttempts'] as string) || fallback + return RATE_LIMIT_MESSAGES[lang] ?? fallback } catch { return fallback } @@ -151,12 +232,12 @@ apiClient.interceptors.response.use( ) export const authApi = { - register: (data: { username: string; email: string; password: string; invite_token?: string }) => apiClient.post('/auth/register', data).then(r => r.data), + register: (data: RegisterRequest) => apiClient.post('/auth/register', data).then(r => r.data), validateInvite: (token: string) => apiClient.get(`/auth/invite/${token}`).then(r => r.data), - login: (data: { email: string; password: string }) => apiClient.post('/auth/login', data).then(r => r.data), - verifyMfaLogin: (data: { mfa_token: string; code: string }) => apiClient.post('/auth/mfa/verify-login', data).then(r => r.data), + login: (data: LoginRequest) => apiClient.post('/auth/login', data).then(r => r.data), + verifyMfaLogin: (data: MfaVerifyLoginRequest) => apiClient.post('/auth/mfa/verify-login', data).then(r => r.data), mfaSetup: () => apiClient.post('/auth/mfa/setup', {}).then(r => r.data), - mfaEnable: (data: { code: string }) => apiClient.post('/auth/mfa/enable', data).then(r => r.data as { success: boolean; mfa_enabled: boolean; backup_codes?: string[] }), + mfaEnable: (data: MfaEnableRequest) => apiClient.post('/auth/mfa/enable', data).then(r => r.data as { success: boolean; mfa_enabled: boolean; backup_codes?: string[] }), mfaDisable: (data: { password: string; code: string }) => apiClient.post('/auth/mfa/disable', data).then(r => r.data), me: () => apiClient.get('/auth/me').then(r => r.data), updateMapsKey: (key: string | null) => apiClient.put('/auth/me/maps-key', { maps_api_key: key }).then(r => r.data), @@ -170,16 +251,34 @@ export const authApi = { updateAppSettings: (data: Record) => apiClient.put('/auth/app-settings', data).then(r => r.data), validateKeys: () => apiClient.get('/auth/validate-keys').then(r => r.data), travelStats: () => apiClient.get('/auth/travel-stats').then(r => r.data), - changePassword: (data: { current_password: string; new_password: string }) => apiClient.put('/auth/me/password', data).then(r => r.data), - forgotPassword: (data: { email: string }) => apiClient.post('/auth/forgot-password', data).then(r => r.data as { ok: true }), - resetPassword: (data: { token: string; new_password: string; mfa_code?: string }) => apiClient.post('/auth/reset-password', data).then(r => r.data as { success?: true; mfa_required?: true }), + changePassword: (data: ChangePasswordRequest) => apiClient.put('/auth/me/password', data).then(r => r.data), + forgotPassword: (data: ForgotPasswordRequest) => apiClient.post('/auth/forgot-password', data).then(r => r.data as { ok: true }), + resetPassword: (data: ResetPasswordRequest) => apiClient.post('/auth/reset-password', data).then(r => r.data as { success?: true; mfa_required?: true }), deleteOwnAccount: () => apiClient.delete('/auth/me').then(r => r.data), demoLogin: () => apiClient.post('/auth/demo-login').then(r => r.data), mcpTokens: { list: () => apiClient.get('/auth/mcp-tokens').then(r => r.data), - create: (name: string) => apiClient.post('/auth/mcp-tokens', { name }).then(r => r.data), + create: (name: string) => apiClient.post('/auth/mcp-tokens', { name } satisfies McpTokenCreateRequest).then(r => r.data), delete: (id: number) => apiClient.delete(`/auth/mcp-tokens/${id}`).then(r => r.data), }, + passkey: { + registerOptions: (password: string) => apiClient.post('/auth/passkey/register/options', { password }).then(r => r.data), + registerVerify: (attestationResponse: unknown, name?: string) => apiClient.post('/auth/passkey/register/verify', { attestationResponse, name }).then(r => r.data), + loginOptions: () => apiClient.post('/auth/passkey/login/options', {}).then(r => r.data), + loginVerify: (assertionResponse: unknown) => apiClient.post('/auth/passkey/login/verify', { assertionResponse }).then(r => r.data as { token: string; user: Record }), + list: () => apiClient.get('/auth/passkey/credentials').then(r => r.data as { credentials: PasskeyCredential[] }), + rename: (id: number, name: string) => apiClient.patch(`/auth/passkey/credentials/${id}`, { name }).then(r => r.data), + delete: (id: number, password: string) => apiClient.delete(`/auth/passkey/credentials/${id}`, { data: { password } }).then(r => r.data), + }, +} + +export interface PasskeyCredential { + id: number + name: string | null + device_type: string | null + backed_up: boolean + created_at: string + last_used_at: string | null } export const oauthApi = { @@ -223,32 +322,33 @@ export const oauthApi = { export const tripsApi = { list: (params?: Record) => apiClient.get('/trips', { params }).then(r => r.data), - create: (data: Record) => apiClient.post('/trips', data).then(r => r.data), + create: (data: TripCreateRequest) => apiClient.post('/trips', data).then(r => r.data), get: (id: number | string) => apiClient.get(`/trips/${id}`).then(r => r.data), - update: (id: number | string, data: Record) => apiClient.put(`/trips/${id}`, data).then(r => r.data), + update: (id: number | string, data: TripUpdateRequest) => apiClient.put(`/trips/${id}`, data).then(r => r.data), delete: (id: number | string) => apiClient.delete(`/trips/${id}`).then(r => r.data), uploadCover: (id: number | string, formData: FormData) => apiClient.post(`/trips/${id}/cover`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(r => r.data), archive: (id: number | string) => apiClient.put(`/trips/${id}`, { is_archived: true }).then(r => r.data), unarchive: (id: number | string) => apiClient.put(`/trips/${id}`, { is_archived: false }).then(r => r.data), getMembers: (id: number | string) => apiClient.get(`/trips/${id}/members`).then(r => r.data), - addMember: (id: number | string, identifier: string) => apiClient.post(`/trips/${id}/members`, { identifier }).then(r => r.data), + addMember: (id: number | string, identifier: string) => apiClient.post(`/trips/${id}/members`, { identifier } satisfies TripAddMemberRequest).then(r => r.data), removeMember: (id: number | string, userId: number) => apiClient.delete(`/trips/${id}/members/${userId}`).then(r => r.data), - copy: (id: number | string, data?: { title?: string }) => apiClient.post(`/trips/${id}/copy`, data || {}).then(r => r.data), + copy: (id: number | string, data?: TripCopyRequest) => apiClient.post(`/trips/${id}/copy`, data || {}).then(r => r.data), bundle: (id: number | string) => apiClient.get(`/trips/${id}/bundle`).then(r => r.data), } export const daysApi = { list: (tripId: number | string) => apiClient.get(`/trips/${tripId}/days`).then(r => r.data), - create: (tripId: number | string, data: Record) => apiClient.post(`/trips/${tripId}/days`, data).then(r => r.data), - update: (tripId: number | string, dayId: number | string, data: Record) => apiClient.put(`/trips/${tripId}/days/${dayId}`, data).then(r => r.data), + create: (tripId: number | string, data: DayCreateRequest) => apiClient.post(`/trips/${tripId}/days`, data).then(r => r.data), + update: (tripId: number | string, dayId: number | string, data: DayUpdateRequest) => apiClient.put(`/trips/${tripId}/days/${dayId}`, data).then(r => r.data), delete: (tripId: number | string, dayId: number | string) => apiClient.delete(`/trips/${tripId}/days/${dayId}`).then(r => r.data), + reorder: (tripId: number | string, orderedIds: number[]) => apiClient.put(`/trips/${tripId}/days/reorder`, { orderedIds } satisfies DayReorderRequest).then(r => r.data), } export const placesApi = { list: (tripId: number | string, params?: Record) => apiClient.get(`/trips/${tripId}/places`, { params }).then(r => r.data), - create: (tripId: number | string, data: Record) => apiClient.post(`/trips/${tripId}/places`, data).then(r => r.data), + create: (tripId: number | string, data: PlaceCreateRequest) => apiClient.post(`/trips/${tripId}/places`, data).then(r => r.data), get: (tripId: number | string, id: number | string) => apiClient.get(`/trips/${tripId}/places/${id}`).then(r => r.data), - update: (tripId: number | string, id: number | string, data: Record) => apiClient.put(`/trips/${tripId}/places/${id}`, data).then(r => r.data), + update: (tripId: number | string, id: number | string, data: PlaceUpdateRequest) => apiClient.put(`/trips/${tripId}/places/${id}`, data).then(r => r.data), delete: (tripId: number | string, id: number | string) => apiClient.delete(`/trips/${tripId}/places/${id}`).then(r => r.data), searchImage: (tripId: number | string, id: number | string) => apiClient.get(`/trips/${tripId}/places/${id}/image`).then(r => r.data), importGpx: (tripId: number | string, file: File, opts?: { waypoints?: boolean; routes?: boolean; tracks?: boolean }) => { @@ -266,65 +366,66 @@ export const placesApi = { if (opts?.paths !== undefined) fd.append('importPaths', String(opts.paths)) return apiClient.post(`/trips/${tripId}/places/import/map`, fd, { headers: { 'Content-Type': 'multipart/form-data' } }).then(r => r.data) }, - importGoogleList: (tripId: number | string, url: string) => - apiClient.post(`/trips/${tripId}/places/import/google-list`, { url }).then(r => r.data), - importNaverList: (tripId: number | string, url: string) => - apiClient.post(`/trips/${tripId}/places/import/naver-list`, { url }).then(r => r.data), + importGoogleList: (tripId: number | string, url: string, enrich?: boolean) => + apiClient.post(`/trips/${tripId}/places/import/google-list`, { url, enrich } satisfies PlaceImportListRequest).then(r => r.data), + importNaverList: (tripId: number | string, url: string, enrich?: boolean) => + apiClient.post(`/trips/${tripId}/places/import/naver-list`, { url, enrich } satisfies PlaceImportListRequest).then(r => r.data), bulkDelete: (tripId: number | string, ids: number[]) => - apiClient.post(`/trips/${tripId}/places/bulk-delete`, { ids }).then(r => r.data), + apiClient.post(`/trips/${tripId}/places/bulk-delete`, { ids } satisfies PlaceBulkDeleteRequest).then(r => r.data), } export const assignmentsApi = { list: (tripId: number | string, dayId: number | string) => apiClient.get(`/trips/${tripId}/days/${dayId}/assignments`).then(r => r.data), - create: (tripId: number | string, dayId: number | string, data: { place_id: number | string }) => apiClient.post(`/trips/${tripId}/days/${dayId}/assignments`, data).then(r => r.data), + create: (tripId: number | string, dayId: number | string, data: AssignmentCreateRequest) => apiClient.post(`/trips/${tripId}/days/${dayId}/assignments`, data).then(r => r.data), delete: (tripId: number | string, dayId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/days/${dayId}/assignments/${id}`).then(r => r.data), - reorder: (tripId: number | string, dayId: number | string, orderedIds: number[]) => apiClient.put(`/trips/${tripId}/days/${dayId}/assignments/reorder`, { orderedIds }).then(r => r.data), + reorder: (tripId: number | string, dayId: number | string, orderedIds: number[]) => apiClient.put(`/trips/${tripId}/days/${dayId}/assignments/reorder`, { orderedIds } satisfies AssignmentReorderRequest).then(r => r.data), move: (tripId: number | string, assignmentId: number, newDayId: number | string, orderIndex: number | null) => apiClient.put(`/trips/${tripId}/assignments/${assignmentId}/move`, { new_day_id: newDayId, order_index: orderIndex }).then(r => r.data), update: (tripId: number | string, dayId: number | string, id: number, data: Record) => apiClient.put(`/trips/${tripId}/days/${dayId}/assignments/${id}`, data).then(r => r.data), getParticipants: (tripId: number | string, id: number) => apiClient.get(`/trips/${tripId}/assignments/${id}/participants`).then(r => r.data), - setParticipants: (tripId: number | string, id: number, userIds: number[]) => apiClient.put(`/trips/${tripId}/assignments/${id}/participants`, { user_ids: userIds }).then(r => r.data), - updateTime: (tripId: number | string, id: number, times: Record) => apiClient.put(`/trips/${tripId}/assignments/${id}/time`, times).then(r => r.data), + setParticipants: (tripId: number | string, id: number, userIds: number[]) => apiClient.put(`/trips/${tripId}/assignments/${id}/participants`, { user_ids: userIds } satisfies AssignmentParticipantsRequest).then(r => r.data), + updateTime: (tripId: number | string, id: number, times: AssignmentTimeRequest) => apiClient.put(`/trips/${tripId}/assignments/${id}/time`, times).then(r => r.data), } export const packingApi = { list: (tripId: number | string) => apiClient.get(`/trips/${tripId}/packing`).then(r => r.data), - create: (tripId: number | string, data: Record) => apiClient.post(`/trips/${tripId}/packing`, data).then(r => r.data), - bulkImport: (tripId: number | string, items: { name: string; category?: string; quantity?: number }[]) => apiClient.post(`/trips/${tripId}/packing/import`, { items }).then(r => r.data), - update: (tripId: number | string, id: number, data: Record) => apiClient.put(`/trips/${tripId}/packing/${id}`, data).then(r => r.data), + create: (tripId: number | string, data: PackingCreateItemRequest) => apiClient.post(`/trips/${tripId}/packing`, data).then(r => r.data), + bulkImport: (tripId: number | string, items: { name: string; category?: string; quantity?: number }[]) => apiClient.post(`/trips/${tripId}/packing/import`, { items } satisfies PackingImportRequest).then(r => r.data), + update: (tripId: number | string, id: number, data: PackingUpdateItemRequest) => apiClient.put(`/trips/${tripId}/packing/${id}`, data).then(r => r.data), delete: (tripId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/packing/${id}`).then(r => r.data), - reorder: (tripId: number | string, orderedIds: number[]) => apiClient.put(`/trips/${tripId}/packing/reorder`, { orderedIds }).then(r => r.data), + reorder: (tripId: number | string, orderedIds: number[]) => apiClient.put(`/trips/${tripId}/packing/reorder`, { orderedIds } satisfies PackingReorderRequest).then(r => r.data), getCategoryAssignees: (tripId: number | string) => apiClient.get(`/trips/${tripId}/packing/category-assignees`).then(r => r.data), - setCategoryAssignees: (tripId: number | string, categoryName: string, userIds: number[]) => apiClient.put(`/trips/${tripId}/packing/category-assignees/${encodeURIComponent(categoryName)}`, { user_ids: userIds }).then(r => r.data), + setCategoryAssignees: (tripId: number | string, categoryName: string, userIds: number[]) => apiClient.put(`/trips/${tripId}/packing/category-assignees/${encodeURIComponent(categoryName)}`, { user_ids: userIds } satisfies PackingCategoryAssigneesRequest).then(r => r.data), + listTemplates: (tripId: number | string) => apiClient.get(`/trips/${tripId}/packing/templates`).then(r => r.data), applyTemplate: (tripId: number | string, templateId: number) => apiClient.post(`/trips/${tripId}/packing/apply-template/${templateId}`).then(r => r.data), saveAsTemplate: (tripId: number | string, name: string) => apiClient.post(`/trips/${tripId}/packing/save-as-template`, { name }).then(r => r.data), - setBagMembers: (tripId: number | string, bagId: number, userIds: number[]) => apiClient.put(`/trips/${tripId}/packing/bags/${bagId}/members`, { user_ids: userIds }).then(r => r.data), + setBagMembers: (tripId: number | string, bagId: number, userIds: number[]) => apiClient.put(`/trips/${tripId}/packing/bags/${bagId}/members`, { user_ids: userIds } satisfies PackingBagMembersRequest).then(r => r.data), listBags: (tripId: number | string) => apiClient.get(`/trips/${tripId}/packing/bags`).then(r => r.data), - createBag: (tripId: number | string, data: { name: string; color?: string }) => apiClient.post(`/trips/${tripId}/packing/bags`, data).then(r => r.data), - updateBag: (tripId: number | string, bagId: number, data: Record) => apiClient.put(`/trips/${tripId}/packing/bags/${bagId}`, data).then(r => r.data), + createBag: (tripId: number | string, data: PackingCreateBagRequest) => apiClient.post(`/trips/${tripId}/packing/bags`, data).then(r => r.data), + updateBag: (tripId: number | string, bagId: number, data: PackingUpdateBagRequest) => apiClient.put(`/trips/${tripId}/packing/bags/${bagId}`, data).then(r => r.data), deleteBag: (tripId: number | string, bagId: number) => apiClient.delete(`/trips/${tripId}/packing/bags/${bagId}`).then(r => r.data), } export const todoApi = { list: (tripId: number | string) => apiClient.get(`/trips/${tripId}/todo`).then(r => r.data), - create: (tripId: number | string, data: Record) => apiClient.post(`/trips/${tripId}/todo`, data).then(r => r.data), - update: (tripId: number | string, id: number, data: Record) => apiClient.put(`/trips/${tripId}/todo/${id}`, data).then(r => r.data), + create: (tripId: number | string, data: TodoCreateItemRequest) => apiClient.post(`/trips/${tripId}/todo`, data).then(r => r.data), + update: (tripId: number | string, id: number, data: TodoUpdateItemRequest) => apiClient.put(`/trips/${tripId}/todo/${id}`, data).then(r => r.data), delete: (tripId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/todo/${id}`).then(r => r.data), - reorder: (tripId: number | string, orderedIds: number[]) => apiClient.put(`/trips/${tripId}/todo/reorder`, { orderedIds }).then(r => r.data), + reorder: (tripId: number | string, orderedIds: number[]) => apiClient.put(`/trips/${tripId}/todo/reorder`, { orderedIds } satisfies TodoReorderRequest).then(r => r.data), getCategoryAssignees: (tripId: number | string) => apiClient.get(`/trips/${tripId}/todo/category-assignees`).then(r => r.data), - setCategoryAssignees: (tripId: number | string, categoryName: string, userIds: number[]) => apiClient.put(`/trips/${tripId}/todo/category-assignees/${encodeURIComponent(categoryName)}`, { user_ids: userIds }).then(r => r.data), + setCategoryAssignees: (tripId: number | string, categoryName: string, userIds: number[]) => apiClient.put(`/trips/${tripId}/todo/category-assignees/${encodeURIComponent(categoryName)}`, { user_ids: userIds } satisfies TodoCategoryAssigneesRequest).then(r => r.data), } export const tagsApi = { list: () => apiClient.get('/tags').then(r => r.data), - create: (data: Record) => apiClient.post('/tags', data).then(r => r.data), - update: (id: number, data: Record) => apiClient.put(`/tags/${id}`, data).then(r => r.data), + create: (data: CreateTagRequest) => apiClient.post('/tags', data).then(r => r.data), + update: (id: number, data: UpdateTagRequest) => apiClient.put(`/tags/${id}`, data).then(r => r.data), delete: (id: number) => apiClient.delete(`/tags/${id}`).then(r => r.data), } export const categoriesApi = { list: () => apiClient.get('/categories').then(r => r.data), - create: (data: Record) => apiClient.post('/categories', data).then(r => r.data), - update: (id: number, data: Record) => apiClient.put(`/categories/${id}`, data).then(r => r.data), + create: (data: CreateCategoryRequest) => apiClient.post('/categories', data).then(r => r.data), + update: (id: number, data: UpdateCategoryRequest) => apiClient.put(`/categories/${id}`, data).then(r => r.data), delete: (id: number) => apiClient.delete(`/categories/${id}`).then(r => r.data), } @@ -333,6 +434,7 @@ export const adminApi = { createUser: (data: Record) => apiClient.post('/admin/users', data).then(r => r.data), updateUser: (id: number, data: Record) => apiClient.put(`/admin/users/${id}`, data).then(r => r.data), deleteUser: (id: number) => apiClient.delete(`/admin/users/${id}`).then(r => r.data), + resetUserPasskeys: (id: number) => apiClient.delete(`/admin/users/${id}/passkeys`).then(r => r.data), stats: () => apiClient.get('/admin/stats').then(r => r.data), saveDemoBaseline: () => apiClient.post('/admin/save-demo-baseline').then(r => r.data), getOidc: () => apiClient.get('/admin/oidc').then(r => r.data), @@ -385,9 +487,23 @@ export const addonsApi = { enabled: () => apiClient.get('/addons').then(r => r.data), } +export const airtrailApi = { + getSettings: () => apiClient.get('/integrations/airtrail/settings').then(r => r.data), + saveSettings: (data: { url: string; apiKey?: string; allowInsecureTls?: boolean }) => + apiClient.put('/integrations/airtrail/settings', data).then(r => r.data), + status: () => apiClient.get('/integrations/airtrail/status').then(r => r.data), + test: (data: { url?: string; apiKey?: string; allowInsecureTls?: boolean }) => + apiClient.post('/integrations/airtrail/test', data).then(r => r.data), + sync: (): Promise<{ changed: number }> => apiClient.post('/integrations/airtrail/sync').then(r => r.data), + // flights + import are added with the trip-planner import (P2) + flights: () => apiClient.get('/integrations/airtrail/flights').then(r => r.data), + import: (tripId: number, flightIds: string[]) => + apiClient.post(`/trips/${tripId}/reservations/import/airtrail`, { flightIds }).then(r => r.data), +} + export const journeyApi = { list: () => apiClient.get('/journeys').then(r => r.data), - create: (data: { title: string; subtitle?: string; trip_ids?: number[] }) => apiClient.post('/journeys', data).then(r => r.data), + create: (data: JourneyCreateRequest) => apiClient.post('/journeys', data).then(r => r.data), get: (id: number) => apiClient.get(`/journeys/${id}`).then(r => r.data), update: (id: number, data: Record) => apiClient.patch(`/journeys/${id}`, data).then(r => r.data), delete: (id: number) => apiClient.delete(`/journeys/${id}`).then(r => r.data), @@ -396,7 +512,7 @@ export const journeyApi = { availableTrips: () => apiClient.get('/journeys/available-trips').then(r => r.data), // Trips (sync sources) - addTrip: (id: number, tripId: number) => apiClient.post(`/journeys/${id}/trips`, { trip_id: tripId }).then(r => r.data), + addTrip: (id: number, tripId: number) => apiClient.post(`/journeys/${id}/trips`, { trip_id: tripId } satisfies JourneyAddTripRequest).then(r => r.data), removeTrip: (id: number, tripId: number) => apiClient.delete(`/journeys/${id}/trips/${tripId}`).then(r => r.data), // Entries @@ -404,7 +520,7 @@ export const journeyApi = { createEntry: (id: number, data: Record) => apiClient.post(`/journeys/${id}/entries`, data).then(r => r.data), updateEntry: (entryId: number, data: Record) => apiClient.patch(`/journeys/entries/${entryId}`, data).then(r => r.data), deleteEntry: (entryId: number) => apiClient.delete(`/journeys/entries/${entryId}`).then(r => r.data), - reorderEntries: (journeyId: number, orderedIds: number[]) => apiClient.put(`/journeys/${journeyId}/entries/reorder`, { orderedIds }).then(r => r.data), + reorderEntries: (journeyId: number, orderedIds: number[]) => apiClient.put(`/journeys/${journeyId}/entries/reorder`, { orderedIds } satisfies JourneyReorderEntriesRequest).then(r => r.data), // Photos uploadPhotos: (entryId: number, formData: FormData, opts?: { onUploadProgress?: (e: import('axios').AxiosProgressEvent) => void; idempotencyKey?: string; signal?: AbortSignal }) => @@ -421,7 +537,7 @@ export const journeyApi = { onUploadProgress: opts?.onUploadProgress, signal: opts?.signal, }).then(r => r.data), - addProviderPhotosToGallery: (journeyId: number, provider: string, assetIds: string[], passphrase?: string) => apiClient.post(`/journeys/${journeyId}/gallery/provider-photos`, { provider, asset_ids: assetIds, ...(passphrase ? { passphrase } : {}) }).then(r => r.data), + addProviderPhotosToGallery: (journeyId: number, provider: string, assetIds: string[], passphrase?: string) => apiClient.post(`/journeys/${journeyId}/gallery/provider-photos`, { provider, asset_ids: assetIds, ...(passphrase ? { passphrase } : {}) } satisfies JourneyProviderPhotosRequest).then(r => r.data), addProviderPhoto: (entryId: number, provider: string, assetId: string, caption?: string, passphrase?: string) => apiClient.post(`/journeys/entries/${entryId}/provider-photos`, { provider, asset_id: assetId, caption, ...(passphrase ? { passphrase } : {}) }).then(r => r.data), addProviderPhotos: (entryId: number, provider: string, assetIds: string[], caption?: string, passphrase?: string) => apiClient.post(`/journeys/entries/${entryId}/provider-photos`, { provider, asset_ids: assetIds, caption, ...(passphrase ? { passphrase } : {}) }).then(r => r.data), linkPhoto: (entryId: number, journeyPhotoId: number) => apiClient.post(`/journeys/entries/${entryId}/link-photo`, { journey_photo_id: journeyPhotoId }).then(r => r.data), @@ -443,19 +559,24 @@ export const journeyApi = { // Share getShareLink: (id: number) => apiClient.get(`/journeys/${id}/share-link`).then(r => r.data), - createShareLink: (id: number, perms: { share_timeline?: boolean; share_gallery?: boolean; share_map?: boolean }) => apiClient.post(`/journeys/${id}/share-link`, perms).then(r => r.data), + createShareLink: (id: number, perms: JourneyShareLinkRequest) => apiClient.post(`/journeys/${id}/share-link`, perms).then(r => r.data), deleteShareLink: (id: number) => apiClient.delete(`/journeys/${id}/share-link`).then(r => r.data), getPublicJourney: (token: string) => apiClient.get(`/public/journey/${token}`).then(r => r.data), } export const mapsApi = { - search: (query: string, lang?: string) => apiClient.post(`/maps/search?lang=${lang || 'en'}`, { query }).then(r => r.data), + search: (query: string, lang?: string) => apiClient.post(`/maps/search?lang=${lang || 'en'}`, { query }).then(r => checkInDev(mapsSearchResultSchema, r.data, 'maps.search')), autocomplete: (input: string, lang?: string, locationBias?: { low: { lat: number; lng: number }; high: { lat: number; lng: number } }, signal?: AbortSignal) => - apiClient.post('/maps/autocomplete', { input, lang, locationBias }, { signal }).then(r => r.data), - details: (placeId: string, lang?: string) => apiClient.get(`/maps/details/${encodeURIComponent(placeId)}`, { params: { lang } }).then(r => r.data), - placePhoto: (placeId: string, lat?: number, lng?: number, name?: string) => apiClient.get(`/maps/place-photo/${encodeURIComponent(placeId)}`, { params: { lat, lng, name } }).then(r => r.data), - reverse: (lat: number, lng: number, lang?: string) => apiClient.get('/maps/reverse', { params: { lat, lng, lang } }).then(r => r.data), - resolveUrl: (url: string) => apiClient.post('/maps/resolve-url', { url }).then(r => r.data), + apiClient.post('/maps/autocomplete', { input, lang, locationBias }, { signal }).then(r => checkInDev(mapsAutocompleteResultSchema, r.data, 'maps.autocomplete')), + details: (placeId: string, lang?: string) => apiClient.get(`/maps/details/${encodeURIComponent(placeId)}`, { params: { lang } }).then(r => checkInDev(mapsPlaceDetailsResultSchema, r.data, 'maps.details')), + placePhoto: (placeId: string, lat?: number, lng?: number, name?: string) => apiClient.get(`/maps/place-photo/${encodeURIComponent(placeId)}`, { params: { lat, lng, name } }).then(r => checkInDev(mapsPlacePhotoResultSchema, r.data, 'maps.placePhoto')), + reverse: (lat: number, lng: number, lang?: string) => apiClient.get('/maps/reverse', { params: { lat, lng, lang } }).then(r => checkInDev(mapsReverseResultSchema, r.data, 'maps.reverse')), + resolveUrl: (url: string) => apiClient.post('/maps/resolve-url', { url }).then(r => checkInDev(mapsResolveUrlResultSchema, r.data, 'maps.resolveUrl')), + // OSM-only POI explore: places of a category within the current map viewport bbox. + // Overpass can be slow on a fresh (uncached) area, so this call gets a longer + // timeout than the global default instead of aborting at 8s and showing nothing. + pois: (category: string, bbox: { south: number; west: number; north: number; east: number }, signal?: AbortSignal) => + apiClient.get('/maps/pois', { params: { category, ...bbox }, signal, timeout: 20000 }).then(r => r.data as { pois: import('../components/Map/poiCategories').Poi[]; source: string; truncated: boolean; clamped?: boolean }), } export const airportsApi = { @@ -465,15 +586,18 @@ export const airportsApi = { export const budgetApi = { list: (tripId: number | string) => apiClient.get(`/trips/${tripId}/budget`).then(r => r.data), - create: (tripId: number | string, data: Record) => apiClient.post(`/trips/${tripId}/budget`, data).then(r => r.data), - update: (tripId: number | string, id: number, data: Record) => apiClient.put(`/trips/${tripId}/budget/${id}`, data).then(r => r.data), + create: (tripId: number | string, data: BudgetCreateItemRequest) => apiClient.post(`/trips/${tripId}/budget`, data).then(r => r.data), + update: (tripId: number | string, id: number, data: BudgetUpdateItemRequest) => apiClient.put(`/trips/${tripId}/budget/${id}`, data).then(r => r.data), delete: (tripId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/budget/${id}`).then(r => r.data), - setMembers: (tripId: number | string, id: number, userIds: number[]) => apiClient.put(`/trips/${tripId}/budget/${id}/members`, { user_ids: userIds }).then(r => r.data), - togglePaid: (tripId: number | string, id: number, userId: number, paid: boolean) => apiClient.put(`/trips/${tripId}/budget/${id}/members/${userId}/paid`, { paid }).then(r => r.data), + setMembers: (tripId: number | string, id: number, userIds: number[]) => apiClient.put(`/trips/${tripId}/budget/${id}/members`, { user_ids: userIds } satisfies BudgetUpdateMembersRequest).then(r => r.data), + togglePaid: (tripId: number | string, id: number, userId: number, paid: boolean) => apiClient.put(`/trips/${tripId}/budget/${id}/members/${userId}/paid`, { paid } satisfies BudgetToggleMemberPaidRequest).then(r => r.data), + setPayers: (tripId: number | string, id: number, payers: { user_id: number; amount: number }[]) => apiClient.put(`/trips/${tripId}/budget/${id}/payers`, { payers }).then(r => r.data), perPersonSummary: (tripId: number | string) => apiClient.get(`/trips/${tripId}/budget/summary/per-person`).then(r => r.data), - settlement: (tripId: number | string) => apiClient.get(`/trips/${tripId}/budget/settlement`).then(r => r.data), + settlement: (tripId: number | string, base?: string) => apiClient.get(`/trips/${tripId}/budget/settlement`, base ? { params: { base } } : undefined).then(r => r.data), + createSettlement: (tripId: number | string, data: { from_user_id: number; to_user_id: number; amount: number }) => apiClient.post(`/trips/${tripId}/budget/settlements`, data).then(r => r.data), + deleteSettlement: (tripId: number | string, settlementId: number) => apiClient.delete(`/trips/${tripId}/budget/settlements/${settlementId}`).then(r => r.data), reorderItems: (tripId: number | string, orderedIds: number[]) => apiClient.put(`/trips/${tripId}/budget/reorder/items`, { orderedIds }).then(r => r.data), - reorderCategories: (tripId: number | string, orderedCategories: string[]) => apiClient.put(`/trips/${tripId}/budget/reorder/categories`, { orderedCategories }).then(r => r.data), + reorderCategories: (tripId: number | string, orderedCategories: string[]) => apiClient.put(`/trips/${tripId}/budget/reorder/categories`, { orderedCategories } satisfies BudgetReorderCategoriesRequest).then(r => r.data), } export const filesApi = { @@ -481,28 +605,40 @@ export const filesApi = { upload: (tripId: number | string, formData: FormData) => apiClient.post(`/trips/${tripId}/files`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(r => r.data), - update: (tripId: number | string, id: number, data: Record) => apiClient.put(`/trips/${tripId}/files/${id}`, data).then(r => r.data), + update: (tripId: number | string, id: number, data: FileUpdateRequest) => apiClient.put(`/trips/${tripId}/files/${id}`, data).then(r => r.data), delete: (tripId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/files/${id}`).then(r => r.data), toggleStar: (tripId: number | string, id: number) => apiClient.patch(`/trips/${tripId}/files/${id}/star`).then(r => r.data), restore: (tripId: number | string, id: number) => apiClient.post(`/trips/${tripId}/files/${id}/restore`).then(r => r.data), permanentDelete: (tripId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/files/${id}/permanent`).then(r => r.data), emptyTrash: (tripId: number | string) => apiClient.delete(`/trips/${tripId}/files/trash/empty`).then(r => r.data), - addLink: (tripId: number | string, fileId: number, data: { reservation_id?: number; assignment_id?: number }) => apiClient.post(`/trips/${tripId}/files/${fileId}/link`, data).then(r => r.data), + addLink: (tripId: number | string, fileId: number, data: FileLinkRequest) => apiClient.post(`/trips/${tripId}/files/${fileId}/link`, data).then(r => r.data), removeLink: (tripId: number | string, fileId: number, linkId: number) => apiClient.delete(`/trips/${tripId}/files/${fileId}/link/${linkId}`).then(r => r.data), getLinks: (tripId: number | string, fileId: number) => apiClient.get(`/trips/${tripId}/files/${fileId}/links`).then(r => r.data), } export const reservationsApi = { list: (tripId: number | string) => apiClient.get(`/trips/${tripId}/reservations`).then(r => r.data), - create: (tripId: number | string, data: Record) => apiClient.post(`/trips/${tripId}/reservations`, data).then(r => r.data), - update: (tripId: number | string, id: number, data: Record) => apiClient.put(`/trips/${tripId}/reservations/${id}`, data).then(r => r.data), + upcoming: () => apiClient.get('/reservations/upcoming').then(r => r.data), + create: (tripId: number | string, data: ReservationCreateRequest) => apiClient.post(`/trips/${tripId}/reservations`, data).then(r => r.data), + update: (tripId: number | string, id: number, data: ReservationUpdateRequest) => apiClient.put(`/trips/${tripId}/reservations/${id}`, data).then(r => r.data), delete: (tripId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/reservations/${id}`).then(r => r.data), updatePositions: (tripId: number | string, positions: { id: number; day_plan_position: number }[], dayId?: number) => apiClient.put(`/trips/${tripId}/reservations/positions`, { positions, day_id: dayId }).then(r => r.data), + importBookingPreview: (tripId: number | string, files: File[]): Promise => { + const fd = new FormData() + for (const f of files) fd.append('files', f) + return apiClient.post(`/trips/${tripId}/reservations/import/booking`, fd, { headers: { 'Content-Type': 'multipart/form-data' } }).then(r => r.data) + }, + importBookingConfirm: (tripId: number | string, items: BookingImportPreviewItem[]): Promise => + apiClient.post(`/trips/${tripId}/reservations/import/booking/confirm`, { items }).then(r => r.data), +} + +export const healthApi = { + features: (): Promise<{ bookingImport: boolean }> => apiClient.get('/health/features').then(r => r.data), } export const weatherApi = { - get: (lat: number, lng: number, date: string) => apiClient.get('/weather', { params: { lat, lng, date } }).then(r => r.data), - getDetailed: (lat: number, lng: number, date: string, lang?: string) => apiClient.get('/weather/detailed', { params: { lat, lng, date, lang } }).then(r => r.data), + get: (lat: number, lng: number, date: string): Promise => apiClient.get('/weather', { params: { lat, lng, date } }).then(r => parseInDev(weatherResultSchema, r.data, 'weather.get')), + getDetailed: (lat: number, lng: number, date: string, lang?: string): Promise => apiClient.get('/weather/detailed', { params: { lat, lng, date, lang } }).then(r => parseInDev(weatherResultSchema, r.data, 'weather.getDetailed')), } export const configApi = { @@ -512,40 +648,46 @@ export const configApi = { export const settingsApi = { get: () => apiClient.get('/settings').then(r => r.data), - set: (key: string, value: unknown) => apiClient.put('/settings', { key, value }).then(r => r.data), - setBulk: (settings: Record) => apiClient.post('/settings/bulk', { settings }).then(r => r.data), + set: (key: string, value: unknown) => { + const body: SettingUpsertRequest = { key, value } + return apiClient.put('/settings', body).then(r => r.data) + }, + setBulk: (settings: Record) => { + const body: SettingsBulkRequest = { settings } + return apiClient.post('/settings/bulk', body).then(r => r.data) + }, } export const accommodationsApi = { list: (tripId: number | string) => apiClient.get(`/trips/${tripId}/accommodations`).then(r => r.data), - create: (tripId: number | string, data: Record) => apiClient.post(`/trips/${tripId}/accommodations`, data).then(r => r.data), - update: (tripId: number | string, id: number, data: Record) => apiClient.put(`/trips/${tripId}/accommodations/${id}`, data).then(r => r.data), + create: (tripId: number | string, data: AccommodationCreateRequest) => apiClient.post(`/trips/${tripId}/accommodations`, data).then(r => r.data), + update: (tripId: number | string, id: number, data: AccommodationUpdateRequest) => apiClient.put(`/trips/${tripId}/accommodations/${id}`, data).then(r => r.data), delete: (tripId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/accommodations/${id}`).then(r => r.data), } export const dayNotesApi = { list: (tripId: number | string, dayId: number | string) => apiClient.get(`/trips/${tripId}/days/${dayId}/notes`).then(r => r.data), - create: (tripId: number | string, dayId: number | string, data: Record) => apiClient.post(`/trips/${tripId}/days/${dayId}/notes`, data).then(r => r.data), - update: (tripId: number | string, dayId: number | string, id: number, data: Record) => apiClient.put(`/trips/${tripId}/days/${dayId}/notes/${id}`, data).then(r => r.data), + create: (tripId: number | string, dayId: number | string, data: DayNoteCreateRequest) => apiClient.post(`/trips/${tripId}/days/${dayId}/notes`, data).then(r => r.data), + update: (tripId: number | string, dayId: number | string, id: number, data: DayNoteUpdateRequest) => apiClient.put(`/trips/${tripId}/days/${dayId}/notes/${id}`, data).then(r => r.data), delete: (tripId: number | string, dayId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/days/${dayId}/notes/${id}`).then(r => r.data), } export const collabApi = { getNotes: (tripId: number | string) => apiClient.get(`/trips/${tripId}/collab/notes`).then(r => r.data), - createNote: (tripId: number | string, data: Record) => apiClient.post(`/trips/${tripId}/collab/notes`, data).then(r => r.data), - updateNote: (tripId: number | string, id: number, data: Record) => apiClient.put(`/trips/${tripId}/collab/notes/${id}`, data).then(r => r.data), + createNote: (tripId: number | string, data: CollabNoteCreateRequest) => apiClient.post(`/trips/${tripId}/collab/notes`, data).then(r => r.data), + updateNote: (tripId: number | string, id: number, data: CollabNoteUpdateRequest) => apiClient.put(`/trips/${tripId}/collab/notes/${id}`, data).then(r => r.data), deleteNote: (tripId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/collab/notes/${id}`).then(r => r.data), uploadNoteFile: (tripId: number | string, noteId: number, formData: FormData) => apiClient.post(`/trips/${tripId}/collab/notes/${noteId}/files`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(r => r.data), deleteNoteFile: (tripId: number | string, noteId: number, fileId: number) => apiClient.delete(`/trips/${tripId}/collab/notes/${noteId}/files/${fileId}`).then(r => r.data), getPolls: (tripId: number | string) => apiClient.get(`/trips/${tripId}/collab/polls`).then(r => r.data), - createPoll: (tripId: number | string, data: Record) => apiClient.post(`/trips/${tripId}/collab/polls`, data).then(r => r.data), - votePoll: (tripId: number | string, id: number, optionIndex: number) => apiClient.post(`/trips/${tripId}/collab/polls/${id}/vote`, { option_index: optionIndex }).then(r => r.data), + createPoll: (tripId: number | string, data: CollabPollCreateRequest) => apiClient.post(`/trips/${tripId}/collab/polls`, data).then(r => r.data), + votePoll: (tripId: number | string, id: number, optionIndex: number) => apiClient.post(`/trips/${tripId}/collab/polls/${id}/vote`, { option_index: optionIndex } satisfies CollabPollVoteRequest).then(r => r.data), closePoll: (tripId: number | string, id: number) => apiClient.put(`/trips/${tripId}/collab/polls/${id}/close`).then(r => r.data), deletePoll: (tripId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/collab/polls/${id}`).then(r => r.data), getMessages: (tripId: number | string, before?: string) => apiClient.get(`/trips/${tripId}/collab/messages${before ? `?before=${before}` : ''}`).then(r => r.data), - sendMessage: (tripId: number | string, data: Record) => apiClient.post(`/trips/${tripId}/collab/messages`, data).then(r => r.data), + sendMessage: (tripId: number | string, data: CollabMessageCreateRequest) => apiClient.post(`/trips/${tripId}/collab/messages`, data).then(r => r.data), deleteMessage: (tripId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/collab/messages/${id}`).then(r => r.data), - reactMessage: (tripId: number | string, id: number, emoji: string) => apiClient.post(`/trips/${tripId}/collab/messages/${id}/react`, { emoji }).then(r => r.data), + reactMessage: (tripId: number | string, id: number, emoji: string) => apiClient.post(`/trips/${tripId}/collab/messages/${id}/react`, { emoji } satisfies CollabReactionRequest).then(r => r.data), linkPreview: (tripId: number | string, url: string) => apiClient.get(`/trips/${tripId}/collab/link-preview?url=${encodeURIComponent(url)}`).then(r => r.data), } @@ -586,16 +728,16 @@ export const shareApi = { export const notificationsApi = { getPreferences: () => apiClient.get('/notifications/preferences').then(r => r.data), updatePreferences: (prefs: Record>) => apiClient.put('/notifications/preferences', prefs).then(r => r.data), - testSmtp: (email?: string) => apiClient.post('/notifications/test-smtp', { email }).then(r => r.data), - testWebhook: (url?: string) => apiClient.post('/notifications/test-webhook', { url }).then(r => r.data), - testNtfy: (payload: { topic?: string; server?: string | null; token?: string | null }) => apiClient.post('/notifications/test-ntfy', payload).then(r => r.data), + testSmtp: (email?: string) => apiClient.post('/notifications/test-smtp', { email }).then(r => checkInDev(channelTestResultSchema, r.data, 'notifications.testSmtp')), + testWebhook: (url?: string) => apiClient.post('/notifications/test-webhook', { url }).then(r => checkInDev(channelTestResultSchema, r.data, 'notifications.testWebhook')), + testNtfy: (payload: { topic?: string; server?: string | null; token?: string | null }) => apiClient.post('/notifications/test-ntfy', payload).then(r => checkInDev(channelTestResultSchema, r.data, 'notifications.testNtfy')), } export const inAppNotificationsApi = { - list: (params?: { limit?: number; offset?: number; unread_only?: boolean }) => - apiClient.get('/notifications/in-app', { params }).then(r => r.data), - unreadCount: () => - apiClient.get('/notifications/in-app/unread-count').then(r => r.data), + list: (params?: { limit?: number; offset?: number; unread_only?: boolean }): Promise => + apiClient.get('/notifications/in-app', { params }).then(r => parseInDev(inAppListResultSchema, r.data, 'notifications.list')), + unreadCount: (): Promise => + apiClient.get('/notifications/in-app/unread-count').then(r => parseInDev(unreadCountResultSchema, r.data, 'notifications.unreadCount')), markRead: (id: number) => apiClient.put(`/notifications/in-app/${id}/read`).then(r => r.data), markUnread: (id: number) => @@ -606,7 +748,7 @@ export const inAppNotificationsApi = { apiClient.delete(`/notifications/in-app/${id}`).then(r => r.data), deleteAll: () => apiClient.delete('/notifications/in-app/all').then(r => r.data), - respond: (id: number, response: 'positive' | 'negative') => + respond: (id: number, response: NotificationRespondRequest['response']) => apiClient.post(`/notifications/in-app/${id}/respond`, { response }).then(r => r.data), } diff --git a/client/src/api/websocket.ts b/client/src/api/websocket.ts index 5a07d357..00a1eddf 100644 --- a/client/src/api/websocket.ts +++ b/client/src/api/websocket.ts @@ -20,6 +20,12 @@ export function getSocketId(): string | null { return mySocketId } +/** Trip ids the app currently has open (joined). Used to re-hydrate the active + * trip's store after the network comes back via the `online` event. */ +export function getActiveTrips(): string[] { + return Array.from(activeTrips) +} + export function setRefetchCallback(fn: RefetchCallback | null): void { refetchCallback = fn } diff --git a/client/src/components/Admin/AddonManager.tsx b/client/src/components/Admin/AddonManager.tsx index c2db2218..458adbf5 100644 --- a/client/src/components/Admin/AddonManager.tsx +++ b/client/src/components/Admin/AddonManager.tsx @@ -4,10 +4,10 @@ import { useTranslation } from '../../i18n' import { useSettingsStore } from '../../store/settingsStore' import { useAddonStore } from '../../store/addonStore' import { useToast } from '../shared/Toast' -import { Puzzle, ListChecks, Wallet, FileText, CalendarDays, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen, MessageCircle, StickyNote, BarChart3, Sparkles, Luggage } from 'lucide-react' +import { Puzzle, ListChecks, Wallet, FileText, CalendarDays, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen, MessageCircle, StickyNote, BarChart3, Sparkles, Luggage, Plane } from 'lucide-react' const ICON_MAP = { - ListChecks, Wallet, FileText, CalendarDays, Puzzle, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen, + ListChecks, Wallet, FileText, CalendarDays, Puzzle, Globe, Briefcase, Image, Terminal, Link2, Compass, BookOpen, Plane, } function ImmichIcon({ size = 14 }: { size?: number }) { @@ -158,16 +158,16 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, return (
{/* Header */} -
-
-

{t('admin.addons.title')}

-

+

+
+

{t('admin.addons.title')}

+

{t('admin.addons.subtitleBefore')}TREK{t('admin.addons.subtitleAfter')}

{addons.length === 0 ? ( -
+
{t('admin.addons.noAddons')}
) : ( @@ -175,9 +175,9 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking, {/* Trip Addons */} {tripAddons.length > 0 && (
-
- - +
+ + {t('admin.addons.type.trip')} — {t('admin.addons.tripHint')}
@@ -185,14 +185,14 @@ export default function AddonManager({ bagTrackingEnabled, onToggleBagTracking,
{addon.id === 'packing' && addon.enabled && onToggleBagTracking && ( -
- +
+
-
{t('admin.bagTracking.title')}
-
{t('admin.bagTracking.subtitle')}
+
{t('admin.bagTracking.title')}
+
{t('admin.bagTracking.subtitle')}
- + {bagTrackingEnabled ? t('admin.addons.enabled') : t('admin.addons.disabled')} )} {expanded && hidden > 0 && ( )}
-
+
{session.username}
@@ -164,8 +161,8 @@ export default function AdminMcpTokensPanel() { {/* MCP Tokens */}
-

{t('admin.mcpTokens.sectionTitle')}

-
+

{t('admin.mcpTokens.sectionTitle')}

+
{tokensLoading ? (
@@ -177,8 +174,8 @@ export default function AdminMcpTokensPanel() {
) : ( <> -
+
{t('admin.mcpTokens.tokenName')} {t('admin.mcpTokens.owner')} {t('admin.mcpTokens.created')} @@ -187,13 +184,12 @@ export default function AdminMcpTokensPanel() {
{tokens.map((token, i) => (
+ className={`grid grid-cols-[1fr_auto_auto_auto_auto] items-center gap-x-4 px-4 py-3 ${i < tokens.length - 1 ? 'border-b border-edge' : ''}`}>
-

{token.name}

+

{token.name}

{token.token_prefix}...

-
+
{token.username}
@@ -217,14 +213,14 @@ export default function AdminMcpTokensPanel() { {/* Revoke OAuth session modal */} {revokeConfirmId !== null && ( -
{ if (e.target === e.currentTarget) setRevokeConfirmId(null) }}> -
-

{t('admin.oauthSessions.revokeTitle')}

-

{t('admin.oauthSessions.revokeMessage')}

+
+

{t('admin.oauthSessions.revokeTitle')}

+

{t('admin.oauthSessions.revokeMessage')}

-

+

{t('admin.audit.showing', { count: entries.length, total })}

{loading && entries.length === 0 ? ( -
{t('common.loading')}
+
{t('common.loading')}
) : entries.length === 0 ? ( -
{t('admin.audit.empty')}
+
{t('admin.audit.empty')}
) : ( -
+
- - - - - - - + + + + + + + {entries.map((e) => ( - - - - - - - + + + + + + + ))} @@ -160,8 +159,7 @@ export default function AuditLogPanel({ serverTimezone }: AuditLogPanelProps): R type="button" disabled={loading} onClick={() => loadMore()} - className="text-sm font-medium underline-offset-2 hover:underline disabled:opacity-50" - style={{ color: 'var(--text-secondary)' }} + className="text-sm font-medium underline-offset-2 hover:underline disabled:opacity-50 text-content-secondary" > {t('admin.audit.loadMore')} diff --git a/client/src/components/Admin/BackupPanel.tsx b/client/src/components/Admin/BackupPanel.tsx index 44acdf5f..6dc67c86 100644 --- a/client/src/components/Admin/BackupPanel.tsx +++ b/client/src/components/Admin/BackupPanel.tsx @@ -186,8 +186,8 @@ export default function BackupPanel() {
-

{t('backup.title')}

-

{t('backup.subtitle')}

+

{t('backup.title')}

+

{t('backup.subtitle')}

@@ -310,8 +310,8 @@ export default function BackupPanel() {
-

{t('backup.auto.title')}

-

{t('backup.auto.subtitle')}

+

{t('backup.auto.title')}

+

{t('backup.auto.subtitle')}

@@ -360,7 +360,7 @@ export default function BackupPanel() { handleAutoSettingsChange('hour', parseInt(v, 10))} + onChange={v => handleAutoSettingsChange('hour', parseInt(String(v), 10))} size="sm" options={HOURS.map(h => { let label: string @@ -408,7 +408,7 @@ export default function BackupPanel() { handleAutoSettingsChange('day_of_month', parseInt(v, 10))} + onChange={v => handleAutoSettingsChange('day_of_month', parseInt(String(v), 10))} size="sm" options={DAYS_OF_MONTH.map(d => ({ value: String(d), label: String(d) }))} /> @@ -458,7 +458,8 @@ export default function BackupPanel() { {/* Restore Warning Modal */} {restoreConfirm && (
setRestoreConfirm(null)} >
{/* Red header */}
-
- +
+
-

+

{t('backup.restoreConfirmTitle')}

-

+

{restoreConfirm.filename}

@@ -505,7 +506,8 @@ export default function BackupPanel() { @@ -131,7 +150,6 @@ export default function DefaultUserSettingsTab(): React.ReactElement { lng: 2.3522, address: null, category_id: null, - icon: null, price: null, currency: null, image_url: null, @@ -148,14 +166,14 @@ export default function DefaultUserSettingsTab(): React.ReactElement { }], []) if (!loaded) { - return

Loading…

+ return

Loading…

} const darkMode = defaults.dark_mode return (
-

+

{t('admin.defaultSettings.description')}

@@ -208,22 +226,6 @@ export default function DefaultUserSettingsTab(): React.ReactElement { ))} - {/* Route Calculation */} - {t('settings.routeCalculation')} }> - {([ - { value: true, label: t('settings.on') || 'On' }, - { value: false, label: t('settings.off') || 'Off' }, - ] as const).map(opt => ( - save({ route_calculation: opt.value })} - > - {opt.label} - - ))} - - {/* Blur Booking Codes */} {t('settings.blurBookingCodes')} }> {([ @@ -242,7 +244,7 @@ export default function DefaultUserSettingsTab(): React.ReactElement { {/* Map Tile URL */}
-
+ + {/* ── Map provider / instance-wide Mapbox ───────────────────────── */} +
+ {t('admin.defaultSettings.mapProvider')} } + hint={t('admin.defaultSettings.mapProviderHint')} + > + {([ + { value: 'leaflet', label: t('admin.defaultSettings.providerLeaflet') }, + { value: 'mapbox-gl', label: t('admin.defaultSettings.providerMapbox') }, + ] as const).map(opt => ( + save({ map_provider: opt.value })} + > + {opt.label} + + ))} + + + {defaults.map_provider === 'mapbox-gl' && ( +
+
+ + ) => setMapboxToken(e.target.value)} + onBlur={() => save({ mapbox_access_token: mapboxToken })} + placeholder="pk.eyJ…" + spellCheck={false} + autoComplete="off" + className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" + /> +

{t('admin.defaultSettings.mapboxTokenHint')}

+
+ +
+ + { if (value) { setMapboxStyle(value); save({ mapbox_style: value }) } }} + placeholder={t('admin.defaultSettings.mapboxStylePlaceholder')} + options={MAPBOX_STYLE_PRESETS.map(p => ({ value: p.url, label: p.name }))} + size="sm" + style={{ marginBottom: 8 }} + /> + ) => setMapboxStyle(e.target.value)} + onBlur={() => save({ mapbox_style: mapboxStyle })} + placeholder="mapbox://styles/mapbox/standard" + className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" + /> +
+ + {t('admin.defaultSettings.mapbox3d')} }> + {([ + { value: true, label: t('settings.on') || 'On' }, + { value: false, label: t('settings.off') || 'Off' }, + ] as const).map(opt => ( + save({ mapbox_3d_enabled: opt.value })}> + {opt.label} + + ))} + + + {t('admin.defaultSettings.mapboxQuality')} }> + {([ + { value: true, label: t('settings.on') || 'On' }, + { value: false, label: t('settings.off') || 'Off' }, + ] as const).map(opt => ( + save({ mapbox_quality_mode: opt.value })}> + {opt.label} + + ))} + +
+ )} +
) } diff --git a/client/src/components/Admin/DevNotificationsPanel.tsx b/client/src/components/Admin/DevNotificationsPanel.tsx index 8221b51c..f5322e73 100644 --- a/client/src/components/Admin/DevNotificationsPanel.tsx +++ b/client/src/components/Admin/DevNotificationsPanel.tsx @@ -68,8 +68,7 @@ export default function DevNotificationsPanel(): React.ReactElement {
-

{label}

-

{sub}

+

{label}

+

{sub}

{sending === id && (
@@ -88,15 +87,14 @@ export default function DevNotificationsPanel(): React.ReactElement { ) const SectionTitle = ({ children }: { children: React.ReactNode }) => ( -

{children}

+

{children}

) const TripSelector = () => ( @@ -106,8 +104,7 @@ export default function DevNotificationsPanel(): React.ReactElement { @@ -116,10 +113,10 @@ export default function DevNotificationsPanel(): React.ReactElement { return (
-
+
DEV ONLY
- + Notification Testing
@@ -127,7 +124,7 @@ export default function DevNotificationsPanel(): React.ReactElement { {/* ── Type Testing ─────────────────────────────────────────────────── */}
Type Testing -

+

Test how each in-app notification type renders, sent to yourself.

@@ -175,7 +172,7 @@ export default function DevNotificationsPanel(): React.ReactElement { {trips.length > 0 && (
Trip-Scoped Events -

+

Fires each trip event to all members of the selected trip (excluding yourself).

@@ -228,7 +225,7 @@ export default function DevNotificationsPanel(): React.ReactElement { {users.length > 0 && (
User-Scoped Events -

+

Fires each user event to the selected recipient.

@@ -266,7 +263,7 @@ export default function DevNotificationsPanel(): React.ReactElement { {/* ── Admin-Scoped Events ──────────────────────────────────────────── */}
Admin-Scoped Events -

+

Fires to all admin users.

diff --git a/client/src/components/Admin/GitHubPanel.tsx b/client/src/components/Admin/GitHubPanel.tsx index 7cc3f421..4ea99b0d 100644 --- a/client/src/components/Admin/GitHubPanel.tsx +++ b/client/src/components/Admin/GitHubPanel.tsx @@ -9,6 +9,12 @@ const PER_PAGE = 10 interface GithubRelease { id: number prerelease: boolean + tag_name: string + name: string | null + body: string | null + published_at: string | null + created_at: string + author: { login: string } | null [key: string]: unknown } @@ -67,7 +73,7 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b elements.push(
- - - - - - - - - - - - ) -} - -// ── Chip with custom tooltip ───────────────────────────────────────────────── -interface ChipWithTooltipProps { - label: string - avatarUrl: string | null - size?: number - paid?: boolean - onClick?: () => void -} - -function ChipWithTooltip({ label, avatarUrl, size = 20, paid, onClick }: ChipWithTooltipProps) { - const [hover, setHover] = useState(false) - const [pos, setPos] = useState({ top: 0, left: 0 }) - const ref = useRef(null) - - const onEnter = () => { - if (ref.current) { - const rect = ref.current.getBoundingClientRect() - setPos({ top: rect.top - 6, left: rect.left + rect.width / 2 }) - } - setHover(true) - } - - const borderColor = paid ? '#22c55e' : 'var(--border-primary)' - const bg = paid ? 'rgba(34,197,94,0.15)' : 'var(--bg-tertiary)' - - return ( - <> -
setHover(false)} - onClick={onClick} - style={{ - width: size, height: size, borderRadius: '50%', border: `2px solid ${borderColor}`, - background: bg, display: 'flex', alignItems: 'center', justifyContent: 'center', - fontSize: size * 0.4, fontWeight: 700, color: paid ? '#16a34a' : 'var(--text-muted)', - overflow: 'hidden', flexShrink: 0, cursor: onClick ? 'pointer' : 'default', - transition: 'border-color 0.15s, background 0.15s', - }}> - {avatarUrl - ? - : label?.[0]?.toUpperCase() - } -
- {hover && ReactDOM.createPortal( -
- {label} - {paid && ( - Paid - )} -
, - document.body - )} - - ) -} - -// ── Budget Member Chips (for Persons column) ──────────────────────────────── -interface BudgetMemberChipsProps { - members?: BudgetMember[] - tripMembers?: TripMember[] - onSetMembers: (memberIds: number[]) => void - onTogglePaid?: (userId: number, paid: boolean) => void - compact?: boolean - readOnly?: boolean -} - -function BudgetMemberChips({ members = [], tripMembers = [], onSetMembers, onTogglePaid, compact = true, readOnly = false }: BudgetMemberChipsProps) { - const chipSize = compact ? 20 : 30 - const btnSize = compact ? 18 : 28 - const iconSize = compact ? (members.length > 0 ? 8 : 9) : (members.length > 0 ? 12 : 14) - const [showDropdown, setShowDropdown] = useState(false) - const [dropPos, setDropPos] = useState({ top: 0, left: 0 }) - const btnRef = useRef(null) - const dropRef = useRef(null) - - const openDropdown = useCallback(() => { - if (btnRef.current) { - const rect = btnRef.current.getBoundingClientRect() - setDropPos({ top: rect.bottom + 4, left: rect.left + rect.width / 2 }) - } - setShowDropdown(v => !v) - }, []) - - useEffect(() => { - if (!showDropdown) return - const close = (e) => { - if (dropRef.current && dropRef.current.contains(e.target)) return - if (btnRef.current && btnRef.current.contains(e.target)) return - setShowDropdown(false) - } - document.addEventListener('mousedown', close) - return () => document.removeEventListener('mousedown', close) - }, [showDropdown]) - - const memberIds = members.map(m => m.user_id) - - const toggleMember = (userId) => { - const newIds = memberIds.includes(userId) - ? memberIds.filter(id => id !== userId) - : [...memberIds, userId] - onSetMembers(newIds) - } - - return ( -
- {members.map(m => ( - onTogglePaid(m.user_id, !m.paid) : undefined} - /> - ))} - {!readOnly && ( - - )} - {showDropdown && ReactDOM.createPortal( -
- {tripMembers.map(tm => { - const isActive = memberIds.includes(tm.id) - return ( - - ) - })} -
, - document.body - )} -
- ) -} - -// ── Per-Person Inline (inside total card) ──────────────────────────────────── -interface PerPersonInlineProps { - tripId: number - budgetItems: BudgetItem[] - currency: string - locale: string -} - -const SPLIT_COLORS = [ - { solid: '#6366f1', gradient: 'linear-gradient(135deg, #6366f1, #8b5cf6)' }, - { solid: '#ec4899', gradient: 'linear-gradient(135deg, #ec4899, #f43f5e)' }, - { solid: '#10b981', gradient: 'linear-gradient(135deg, #10b981, #22c55e)' }, - { solid: '#f59e0b', gradient: 'linear-gradient(135deg, #f59e0b, #f97316)' }, - { solid: '#06b6d4', gradient: 'linear-gradient(135deg, #06b6d4, #3b82f6)' }, - { solid: '#a855f7', gradient: 'linear-gradient(135deg, #a855f7, #d946ef)' }, -] - -export function splitColorFor(userId: number, order: number) { - return SPLIT_COLORS[order % SPLIT_COLORS.length] -} - -function colorForUserId(userId: number) { - return SPLIT_COLORS[((userId | 0) - 1 + SPLIT_COLORS.length * 1000) % SPLIT_COLORS.length] -} - -function RingAvatar({ userId, username, avatarUrl, size = 34, innerBg = '#17171d', textColor = '#fff' }: { userId: number; username?: string; avatarUrl?: string | null; size?: number; innerBg?: string; textColor?: string }) { - const color = colorForUserId(userId) - return ( -
-
- {avatarUrl ? : username?.[0]?.toUpperCase()} -
-
- ) -} - -function PerPersonInline({ tripId, budgetItems, currency, locale, grandTotal, theme }: PerPersonInlineProps & { grandTotal: number; theme: ReturnType }) { - const [data, setData] = useState(null) - const fmt = (v: number) => fmtNum(v, locale, currency) - - useEffect(() => { - budgetApi.perPersonSummary(tripId).then(d => setData(d.summary)).catch(() => {}) - }, [tripId, budgetItems]) - - if (!data || data.length === 0) return null - - const people = data.map((p: any) => ({ ...p, color: colorForUserId(p.user_id) })) - - return ( - <> - {grandTotal > 0 && ( -
- {people.map(p => ( -
- ))} -
- )} - -
- {people.map(p => { - const percent = grandTotal > 0 ? Math.round((p.total_assigned / grandTotal) * 100) : 0 - return ( -
- -
-
{p.username}
-
{percent}%
-
-
{fmt(p.total_assigned)}
-
- ) - })} -
- - ) -} - -// ── Pie Chart (pure CSS conic-gradient) ────────────────────────────────────── -interface PieChartProps { - segments: PieSegment[] - size?: number - totalLabel: string -} - -function PieChart({ segments, size = 200, totalLabel }: PieChartProps) { - if (!segments.length) return null - - const total = segments.reduce((s, x) => s + x.value, 0) - if (total === 0) return null - - let cumDeg = 0 - const stops = segments.map(seg => { - const start = cumDeg - const deg = (seg.value / total) * 360 - cumDeg += deg - return `${seg.color} ${start}deg ${start + deg}deg` - }).join(', ') - - return ( -
-
-
- - {totalLabel} -
-
- ) -} +export { splitColorFor } from './BudgetPanel.helpers' // ── Main Component ─────────────────────────────────────────────────────────── interface BudgetPanelProps { @@ -559,127 +15,21 @@ interface BudgetPanelProps { } export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelProps) { - const { trip, budgetItems, addBudgetItem, updateBudgetItem, deleteBudgetItem, loadBudgetItems, updateTrip, setBudgetItemMembers, toggleBudgetMemberPaid, reorderBudgetItems, reorderBudgetCategories } = useTripStore() - const can = useCanDo() - const { t, locale } = useTranslation() - const isDark = useIsDark() - const theme = useMemo(() => widgetTheme(isDark), [isDark]) - const [newCategoryName, setNewCategoryName] = useState('') - const [editingCat, setEditingCat] = useState(null) // { name, value } - const [settlement, setSettlement] = useState<{ balances: any[]; flows: any[] } | null>(null) - const [settlementOpen, setSettlementOpen] = useState(false) - const currency = trip?.currency || 'EUR' - const canEdit = can('budget_edit', trip) - - const fmt = (v, cur) => fmtNum(v, locale, cur) - const hasMultipleMembers = tripMembers.length > 1 - - // Drag state for categories - const [dragCat, setDragCat] = useState(null) - const [dragOverCat, setDragOverCat] = useState(null) - // Drag state for items within a category - const [dragItem, setDragItem] = useState(null) - const [dragOverItem, setDragOverItem] = useState(null) - const [dragItemCat, setDragItemCat] = useState(null) - - // Load settlement data whenever budget items change - useEffect(() => { - if (!hasMultipleMembers) return - budgetApi.settlement(tripId).then(setSettlement).catch(() => {}) - }, [tripId, budgetItems, hasMultipleMembers]) - - const setCurrency = (cur) => { - if (tripId) updateTrip(tripId, { currency: cur }) - } - - useEffect(() => { if (tripId) loadBudgetItems(tripId) }, [tripId]) - - const grouped = useMemo(() => { - const map = new Map() - for (const item of (budgetItems || [])) { - const cat = item.category || 'Other' - if (!map.has(cat)) map.set(cat, []) - map.get(cat)!.push(item) - } - return map - }, [budgetItems]) - - const categoryNames = Array.from(grouped.keys()) - - // Stable color mapping: assign index-based colors once, never reassign on reorder - const colorMapRef = useRef(new Map()) - const categoryColor = useCallback((cat: string) => { - const map = colorMapRef.current - if (!map.has(cat)) { - map.set(cat, PIE_COLORS[map.size % PIE_COLORS.length]) - } - return map.get(cat)! - }, []) - const grandTotal = (budgetItems || []).reduce((s, i) => s + (i.total_price || 0), 0) - - const pieSegments = useMemo(() => - categoryNames.map((cat, i) => ({ - name: cat, - value: (grouped.get(cat) || []).reduce((s, x) => s + (x.total_price || 0), 0), - color: categoryColor(cat), - })).filter(s => s.value > 0) - , [grouped, categoryNames]) - - const handleAddItem = async (category, data) => { try { await addBudgetItem(tripId, { ...data, category }) } catch {} } - const handleUpdateField = async (id, field, value) => { try { await updateBudgetItem(tripId, id, { [field]: value }) } catch {} } - const handleDeleteItem = async (id) => { try { await deleteBudgetItem(tripId, id) } catch {} } - const handleDeleteCategory = async (cat) => { - const items = grouped.get(cat) || [] - for (const item of Array.from(items)) await deleteBudgetItem(tripId, item.id) - } - const handleRenameCategory = async (oldName, newName) => { - if (!newName.trim() || newName.trim() === oldName) return - const items = grouped.get(oldName) || [] - for (const item of Array.from(items)) await updateBudgetItem(tripId, item.id, { category: newName.trim() }) - } - const handleAddCategory = () => { - if (!newCategoryName.trim()) return - addBudgetItem(tripId, { name: t('budget.defaultEntry'), category: newCategoryName.trim(), total_price: 0 }) - setNewCategoryName('') - } - - const handleExportCsv = () => { - const sep = ';' - const esc = (v: any) => { const s = String(v ?? ''); return s.includes(sep) || s.includes('"') || s.includes('\n') ? '"' + s.replace(/"/g, '""') + '"' : s } - const d = currencyDecimals(currency) - const fmtPrice = (v: number | null | undefined) => v != null ? v.toFixed(d) : '' - - const fmtDate = (iso: string) => { if (!iso) return ''; const d = new Date(iso + 'T00:00:00Z'); return d.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: 'UTC' }) } - const header = ['Category', 'Name', 'Date', 'Total (' + currency + ')', 'Persons', 'Days', 'Per Person', 'Per Day', 'Per Person/Day', 'Note'] - const rows = [header.join(sep)] - - for (const cat of categoryNames) { - for (const item of (grouped.get(cat) || [])) { - const pp = calcPP(item.total_price, item.persons) - const pd = calcPD(item.total_price, item.days) - const ppd = calcPPD(item.total_price, item.persons, item.days) - rows.push([ - esc(item.category), esc(item.name), esc(fmtDate(item.expense_date || '')), - fmtPrice(item.total_price), item.persons ?? '', item.days ?? '', - fmtPrice(pp), fmtPrice(pd), fmtPrice(ppd), - esc(item.note || ''), - ].join(sep)) - } - } - - const bom = '\uFEFF' - const blob = new Blob([bom + rows.join('\r\n')], { type: 'text/csv;charset=utf-8;' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - const safeName = (trip?.title || 'trip').replace(/[^a-zA-Z0-9\u00C0-\u024F _-]/g, '').trim() - a.download = `budget-${safeName}.csv` - a.click() - URL.revokeObjectURL(url) - } - - const th = { padding: '6px 8px', textAlign: 'center', fontSize: 11, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', borderBottom: '2px solid var(--border-primary)', whiteSpace: 'nowrap', background: 'var(--bg-secondary)' } - const td = { padding: '2px 6px', borderBottom: '1px solid var(--border-secondary)', fontSize: 13, verticalAlign: 'middle', color: 'var(--text-primary)' } + const { + budgetItems, + setBudgetItemMembers, toggleBudgetMemberPaid, reorderBudgetItems, reorderBudgetCategories, + t, locale, isDark, theme, + newCategoryName, setNewCategoryName, + editingCat, setEditingCat, + settlement, settlementOpen, setSettlementOpen, + currency, canEdit, fmt, hasMultipleMembers, + dragCat, setDragCat, dragOverCat, setDragOverCat, + dragItem, setDragItem, dragOverItem, setDragOverItem, dragItemCat, setDragItemCat, + setCurrency, + grouped, categoryNames, categoryColor, grandTotal, pieSegments, + handleAddItem, handleUpdateField, handleDeleteItem, handleDeleteCategory, handleRenameCategory, handleAddCategory, handleExportCsv, + th, td, + } = useBudgetPanel(tripId, tripMembers) // ── Empty State ────────────────────────────────────────────────────────── if (!budgetItems || budgetItems.length === 0) { @@ -707,7 +57,6 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro } // ── Main Layout ────────────────────────────────────────────────────────── - const totalBudget = budgetItems.reduce((s, x) => s + (x.total_price || 0), 0) return (
@@ -771,462 +120,26 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro
- {categoryNames.map((cat, ci) => { - const items = grouped.get(cat) || [] - const subtotal = items.reduce((s, x) => s + (x.total_price || 0), 0) - const color = categoryColor(cat) - - return ( -
{ - if (!dragCat || dragCat === cat || dragItem) return - e.preventDefault(); e.dataTransfer.dropEffect = 'move' - setDragOverCat(cat) - }} - onDragLeave={e => { - if (!e.currentTarget.contains(e.relatedTarget as Node)) setDragOverCat(null) - }} - onDrop={e => { - e.preventDefault() - if (dragCat && dragCat !== cat) { - const newOrder = [...categoryNames] - const fromIdx = newOrder.indexOf(dragCat) - const toIdx = newOrder.indexOf(cat) - newOrder.splice(fromIdx, 1) - newOrder.splice(toIdx, 0, dragCat) - reorderBudgetCategories(tripId, newOrder) - } - setDragCat(null); setDragOverCat(null) - }} - > - {dragOverCat === cat &&
} -
-
- {canEdit && ( -
{ e.stopPropagation(); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/x-budget-cat', cat); setDragCat(cat) }} - onDragEnd={() => { setDragCat(null); setDragOverCat(null) }} - style={{ cursor: 'grab', display: 'flex', alignItems: 'center', color: 'rgba(255,255,255,0.4)', flexShrink: 0 }}> - -
- )} -
- {canEdit && editingCat?.name === cat ? ( - setEditingCat({ ...editingCat, value: e.target.value })} - onBlur={() => { handleRenameCategory(cat, editingCat.value); setEditingCat(null) }} - onKeyDown={e => { if (e.key === 'Enter') { handleRenameCategory(cat, editingCat.value); setEditingCat(null) } if (e.key === 'Escape') setEditingCat(null) }} - style={{ fontWeight: 600, fontSize: 13, background: 'rgba(255,255,255,0.15)', border: 'none', borderRadius: 4, color: '#fff', padding: '1px 6px', outline: 'none', fontFamily: 'inherit', width: '100%' }} - /> - ) : ( - <> - {cat} - {canEdit && ( - - )} - - )} -
-
- {fmt(subtotal, currency)} - {canEdit && ( - - )} -
-
- -
{ if (dragCat) { e.preventDefault(); e.dataTransfer.dropEffect = 'move' } }}> -
{t('admin.audit.col.time')}{t('admin.audit.col.user')}{t('admin.audit.col.action')}{t('admin.audit.col.resource')}{t('admin.audit.col.ip')}{t('admin.audit.col.details')}
{t('admin.audit.col.time')}{t('admin.audit.col.user')}{t('admin.audit.col.action')}{t('admin.audit.col.resource')}{t('admin.audit.col.ip')}{t('admin.audit.col.details')}
{fmtTime(e.created_at)}{userLabel(e)}{e.action}{e.resource || '—'}{e.ip || '—'}{fmtDetails(e.details)}
{fmtTime(e.created_at)}{userLabel(e)}{e.action}{e.resource || '—'}{e.ip || '—'}{fmtDetails(e.details)}
- setName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} - placeholder={t('budget.newEntry')} style={inp} /> - - setPrice(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} - onPaste={e => { e.preventDefault(); let t = e.clipboardData.getData('text').trim().replace(/[^\d.,-]/g, ''); const lc = t.lastIndexOf(','), ld = t.lastIndexOf('.'), dp = Math.max(lc, ld); if (dp > -1) { t = t.substring(0, dp).replace(/[.,]/g, '') + '.' + t.substring(dp + 1) } else { t = t.replace(/[.,]/g, '') } setPrice(t) }} - placeholder="0,00" inputMode="decimal" style={{ ...inp, textAlign: 'center' }} /> - - setPersons(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} - placeholder="-" inputMode="numeric" style={{ ...inp, textAlign: 'center', maxWidth: 60, margin: '0 auto' }} /> - - setDays(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} - placeholder="-" inputMode="numeric" style={{ ...inp, textAlign: 'center', maxWidth: 60, margin: '0 auto' }} /> - --- -
- -
-
- setNote(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} placeholder={t('budget.table.note')} style={inp} /> - - -
- - - - - - - - - - - - - - - - {items.map(item => { - const pp = calcPP(item.total_price, item.persons) - const pd = calcPD(item.total_price, item.days) - const ppd = calcPPD(item.total_price, item.persons, item.days) - const hasMembers = item.members?.length > 0 - return ( - { - if (dragCat && dragCat !== cat) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; return } - if (dragItem && dragItemCat === cat && dragItem !== item.id) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setDragOverItem(item.id) } - }} - onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setDragOverItem(null) }} - onDrop={e => { - if (dragItem && dragItemCat === cat && dragItem !== item.id) { - e.preventDefault(); e.stopPropagation() - const ids = items.map(i => i.id) - const fromIdx = ids.indexOf(dragItem) - const toIdx = ids.indexOf(item.id) - ids.splice(fromIdx, 1) - ids.splice(toIdx, 0, dragItem) - reorderBudgetItems(tripId, ids) - setDragItem(null); setDragOverItem(null); setDragItemCat(null) - } - }} - onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'} - onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> - - - - - - - - - - - - ) - })} - {canEdit && handleAddItem(cat, data)} t={t} />} - -
{t('budget.table.name')}{t('budget.table.total')}{t('budget.table.persons')}{t('budget.table.days')}{t('budget.table.perPerson')}{t('budget.table.perDay')}{t('budget.table.perPersonDay')}{t('budget.table.date')}{t('budget.table.note')}
-
- {canEdit && ( -
{ e.stopPropagation(); e.dataTransfer.effectAllowed = 'move'; setDragItem(item.id); setDragItemCat(cat) }} - onDragEnd={() => { setDragItem(null); setDragOverItem(null); setDragItemCat(null) }} - style={{ cursor: 'grab', display: 'flex', alignItems: 'center', color: 'var(--text-faint)', flexShrink: 0 }}> - -
- )} -
- handleUpdateField(item.id, 'name', v)} placeholder={t('budget.table.name')} locale={locale} editTooltip={item.reservation_id ? t('budget.linkedToReservation') : t('budget.editTooltip')} readOnly={!canEdit || !!item.reservation_id} /> - {hasMultipleMembers && ( -
- setBudgetItemMembers(tripId, item.id, userIds)} - onTogglePaid={(userId, paid) => toggleBudgetMemberPaid(tripId, item.id, userId, paid)} - compact={false} - readOnly={!canEdit} - /> -
- )} -
-
-
- handleUpdateField(item.id, 'total_price', v)} style={{ textAlign: 'center' }} placeholder={currencyDecimals(currency) === 0 ? '0' : '0,00'} locale={locale} editTooltip={t('budget.editTooltip')} readOnly={!canEdit} /> - - {hasMultipleMembers ? ( - setBudgetItemMembers(tripId, item.id, userIds)} - onTogglePaid={(userId, paid) => toggleBudgetMemberPaid(tripId, item.id, userId, paid)} - readOnly={!canEdit} - /> - ) : ( - handleUpdateField(item.id, 'persons', v != null ? parseInt(v) || null : null)} style={{ textAlign: 'center' }} placeholder="-" locale={locale} editTooltip={t('budget.editTooltip')} readOnly={!canEdit} /> - )} - - handleUpdateField(item.id, 'days', v != null ? parseInt(v) || null : null)} style={{ textAlign: 'center' }} placeholder="-" locale={locale} editTooltip={t('budget.editTooltip')} readOnly={!canEdit} /> - {pp != null ? fmt(pp, currency) : '-'}{pd != null ? fmt(pd, currency) : '-'}{ppd != null ? fmt(ppd, currency) : '-'} - {canEdit ? ( -
- handleUpdateField(item.id, 'expense_date', v || null)} placeholder="—" compact borderless /> -
- ) : ( - {item.expense_date || '—'} - )} -
handleUpdateField(item.id, 'note', v)} placeholder={t('budget.table.note')} locale={locale} editTooltip={t('budget.editTooltip')} readOnly={!canEdit} /> - {canEdit && ( - - )} -
-
-
- ) - })} + {categoryNames.map(cat => ( + + ))}
-
- -
-
-
- -
-
-
{t('budget.totalBudget')}
-
-
- - {(() => { - const decimals = currencyDecimals(currency) - const full = Number(grandTotal).toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) - const sep = (0.1).toLocaleString(locale).replace(/\d/g, '') - const [integerPart, decimalPart] = decimals > 0 ? full.split(sep) : [full, ''] - return ( -
- {integerPart} - {decimalPart && {sep}{decimalPart}} - {SYMBOLS[currency] || currency} -
- ) - })()} -
- {currency} -
- - {hasMultipleMembers && (budgetItems || []).some(i => i.members?.length > 0) && ( - - )} - - {/* Settlement dropdown inside the total card */} - {hasMultipleMembers && settlement && settlement.flows.length > 0 && ( -
- - - {settlementOpen && ( -
- {settlement.flows.map((flow, i) => ( -
{ e.currentTarget.style.background = theme.flowHoverBg; e.currentTarget.style.borderColor = theme.flowHoverBorder }} - onMouseLeave={e => { e.currentTarget.style.background = theme.flowBg; e.currentTarget.style.borderColor = theme.flowBorder }} - > - -
- - {fmt(flow.amount, currency)} - -
-
-
-
- -
- ))} - - {settlement.balances.filter(b => Math.abs(b.balance) > 0.01).length > 0 && ( -
-
- {t('budget.netBalances')} -
-
- {settlement.balances.filter(b => Math.abs(b.balance) > 0.01).map(b => { - const positive = b.balance > 0 - const Trend = positive ? TrendingUp : TrendingDown - return ( -
- - - {b.username} - - - - {positive ? '+' : ''}{fmt(b.balance, currency)} - -
- ) - })} -
-
- )} -
- )} -
- )} -
- - {pieSegments.length > 0 && (() => { - const decimals = currencyDecimals(currency) - const total = pieSegments.reduce((s, x) => s + x.value, 0) - const totalFmt = Number(total).toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) - const decimalSep = (0.1).toLocaleString(locale).replace(/\d/g, '') - const [totalInt, totalDec] = decimals > 0 ? totalFmt.split(decimalSep) : [totalFmt, ''] - const R = 80 - const CIRC = 2 * Math.PI * R - let dashOffset = 0 - return ( -
-
-
- -
-
-
{t('budget.byCategory')}
-
-
- -
- - - {pieSegments.map((seg, i) => { - const c2 = hexLighten(seg.color, 0.2) - return ( - - - - - ) - })} - - - {pieSegments.map((seg, i) => { - const segLen = total > 0 ? (seg.value / total) * CIRC : 0 - const circle = ( - - ) - dashOffset += segLen - return circle - })} - -
-
{t('budget.total')}
-
- {totalInt} - {totalDec && {decimalSep}{totalDec}} -
-
{currency}
-
-
- -
- {pieSegments.map((seg, i) => { - const pct = total > 0 ? (seg.value / total) * 100 : 0 - const pctLabel = pct.toFixed(1).replace('.', decimalSep) + '%' - const c2 = hexLighten(seg.color, 0.2) - const chipColor = isDark ? hexLighten(seg.color, 0.35) : seg.color - return ( -
e.currentTarget.style.background = theme.rowHover} - onMouseLeave={e => e.currentTarget.style.background = 'transparent'} - > -
-
-
{seg.name}
-
{fmt(seg.value, currency)}
-
- {pctLabel} -
- ) - })} -
-
- ) - })()} - -
+
) diff --git a/client/src/components/Budget/BudgetPanelAddItemRow.tsx b/client/src/components/Budget/BudgetPanelAddItemRow.tsx new file mode 100644 index 00000000..544a6650 --- /dev/null +++ b/client/src/components/Budget/BudgetPanelAddItemRow.tsx @@ -0,0 +1,67 @@ +import { useState, useRef } from 'react' +import { Plus } from 'lucide-react' +import { CustomDatePicker } from '../shared/CustomDateTimePicker' + +interface AddItemRowProps { + onAdd: (data: { name: string; total_price: number; persons: number | null; days: number | null; note: string | null; expense_date: string | null }) => void + t: (key: string) => string +} + +export default function AddItemRow({ onAdd, t }: AddItemRowProps) { + const [name, setName] = useState('') + const [price, setPrice] = useState('') + const [persons, setPersons] = useState('') + const [days, setDays] = useState('') + const [note, setNote] = useState('') + const [expenseDate, setExpenseDate] = useState('') + const nameRef = useRef(null) + + const handleAdd = () => { + if (!name.trim()) return + onAdd({ name: name.trim(), total_price: parseFloat(String(price).replace(',', '.')) || 0, persons: parseInt(persons) || null, days: parseInt(days) || null, note: note.trim() || null, expense_date: expenseDate || null }) + setName(''); setPrice(''); setPersons(''); setDays(''); setNote(''); setExpenseDate('') + setTimeout(() => nameRef.current?.focus(), 50) + } + + const inp = { border: '1px solid var(--border-primary)', borderRadius: 4, padding: '4px 6px', fontSize: 13, outline: 'none', fontFamily: 'inherit', width: '100%', background: 'var(--bg-input)', color: 'var(--text-primary)' } + + return ( + + + setName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} + placeholder={t('budget.newEntry')} style={inp} /> + + + setPrice(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} + onPaste={e => { e.preventDefault(); let t = e.clipboardData.getData('text').trim().replace(/[^\d.,-]/g, ''); const lc = t.lastIndexOf(','), ld = t.lastIndexOf('.'), dp = Math.max(lc, ld); if (dp > -1) { t = t.substring(0, dp).replace(/[.,]/g, '') + '.' + t.substring(dp + 1) } else { t = t.replace(/[.,]/g, '') } setPrice(t) }} + placeholder="0,00" inputMode="decimal" style={{ ...inp, textAlign: 'center' }} /> + + + setPersons(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} + placeholder="-" inputMode="numeric" style={{ ...inp, textAlign: 'center', maxWidth: 60, margin: '0 auto' }} /> + + + setDays(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} + placeholder="-" inputMode="numeric" style={{ ...inp, textAlign: 'center', maxWidth: 60, margin: '0 auto' }} /> + + - + - + - + +
+ +
+ + + setNote(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} placeholder={t('budget.table.note')} style={inp} /> + + + + + + ) +} diff --git a/client/src/components/Budget/BudgetPanelCategoryTable.tsx b/client/src/components/Budget/BudgetPanelCategoryTable.tsx new file mode 100644 index 00000000..5eefec43 --- /dev/null +++ b/client/src/components/Budget/BudgetPanelCategoryTable.tsx @@ -0,0 +1,258 @@ +import type { CSSProperties, Dispatch, SetStateAction } from 'react' +import { Trash2, Pencil, GripVertical } from 'lucide-react' +import type { BudgetItem } from '../../types' +import { currencyDecimals } from '../../utils/formatters' +import { CustomDatePicker } from '../shared/CustomDateTimePicker' +import { calcPP, calcPD, calcPPD } from './BudgetPanel.helpers' +import InlineEditCell from './BudgetPanelInlineEditCell' +import AddItemRow from './BudgetPanelAddItemRow' +import BudgetMemberChips, { type TripMember } from './BudgetPanelMemberChips' +import type { EditingCat, AddItemData } from './useBudgetPanel' + +interface BudgetCategoryTableProps { + cat: string + grouped: Map + categoryColor: (cat: string) => string + canEdit: boolean + editingCat: EditingCat | null + setEditingCat: Dispatch> + dragCat: string | null + setDragCat: Dispatch> + dragOverCat: string | null + setDragOverCat: Dispatch> + dragItem: number | null + setDragItem: Dispatch> + dragOverItem: number | null + setDragOverItem: Dispatch> + dragItemCat: string | null + setDragItemCat: Dispatch> + categoryNames: string[] + reorderBudgetCategories: (tripId: number | string, orderedCategories: string[]) => Promise + reorderBudgetItems: (tripId: number | string, orderedIds: number[]) => Promise + handleRenameCategory: (oldName: string, newName: string) => Promise + handleDeleteCategory: (cat: string) => Promise + handleDeleteItem: (id: number) => Promise + handleUpdateField: (id: number, field: string, value: unknown) => Promise + handleAddItem: (category: string, data: AddItemData) => Promise + tripId: number + currency: string + locale: string + t: (key: string) => string + fmt: (v: number | null | undefined, cur: string) => string + hasMultipleMembers: boolean + tripMembers: TripMember[] + setBudgetItemMembers: (tripId: number | string, itemId: number, userIds: number[]) => Promise<{ members: unknown; item: unknown }> + toggleBudgetMemberPaid: (tripId: number | string, itemId: number, userId: number, paid: boolean) => Promise + th: CSSProperties + td: CSSProperties +} + +export default function BudgetCategoryTable({ cat, grouped, categoryColor, canEdit, editingCat, setEditingCat, + dragCat, setDragCat, dragOverCat, setDragOverCat, dragItem, setDragItem, dragOverItem, setDragOverItem, + dragItemCat, setDragItemCat, categoryNames, reorderBudgetCategories, reorderBudgetItems, + handleRenameCategory, handleDeleteCategory, handleDeleteItem, handleUpdateField, handleAddItem, + tripId, currency, locale, t, fmt, hasMultipleMembers, tripMembers, setBudgetItemMembers, toggleBudgetMemberPaid, th, td }: BudgetCategoryTableProps) { + const items = grouped.get(cat) || [] + const subtotal = items.reduce((s, x) => s + (x.total_price || 0), 0) + const color = categoryColor(cat) + return ( +
{ + if (!dragCat || dragCat === cat || dragItem) return + e.preventDefault(); e.dataTransfer.dropEffect = 'move' + setDragOverCat(cat) + }} + onDragLeave={e => { + if (!e.currentTarget.contains(e.relatedTarget as Node)) setDragOverCat(null) + }} + onDrop={e => { + e.preventDefault() + if (dragCat && dragCat !== cat) { + const newOrder = [...categoryNames] + const fromIdx = newOrder.indexOf(dragCat) + const toIdx = newOrder.indexOf(cat) + newOrder.splice(fromIdx, 1) + newOrder.splice(toIdx, 0, dragCat) + reorderBudgetCategories(tripId, newOrder) + } + setDragCat(null); setDragOverCat(null) + }} + > + {dragOverCat === cat &&
} +
+
+ {canEdit && ( +
{ e.stopPropagation(); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/x-budget-cat', cat); setDragCat(cat) }} + onDragEnd={() => { setDragCat(null); setDragOverCat(null) }} + style={{ cursor: 'grab', display: 'flex', alignItems: 'center', color: 'rgba(255,255,255,0.4)', flexShrink: 0 }}> + +
+ )} +
+ {canEdit && editingCat?.name === cat ? ( + setEditingCat({ ...editingCat, value: e.target.value })} + onBlur={() => { handleRenameCategory(cat, editingCat.value); setEditingCat(null) }} + onKeyDown={e => { if (e.key === 'Enter') { handleRenameCategory(cat, editingCat.value); setEditingCat(null) } if (e.key === 'Escape') setEditingCat(null) }} + style={{ fontWeight: 600, fontSize: 13, background: 'rgba(255,255,255,0.15)', border: 'none', borderRadius: 4, color: '#fff', padding: '1px 6px', outline: 'none', fontFamily: 'inherit', width: '100%' }} + /> + ) : ( + <> + {cat} + {canEdit && ( + + )} + + )} +
+
+ {fmt(subtotal, currency)} + {canEdit && ( + + )} +
+
+ +
{ if (dragCat) { e.preventDefault(); e.dataTransfer.dropEffect = 'move' } }}> + + + + + + + + + + + + + + + + + {items.map(item => { + const pp = calcPP(item.total_price, item.persons) + const pd = calcPD(item.total_price, item.days) + const ppd = calcPPD(item.total_price, item.persons, item.days) + const hasMembers = (item.members?.length ?? 0) > 0 + return ( + { + if (dragCat && dragCat !== cat) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; return } + if (dragItem && dragItemCat === cat && dragItem !== item.id) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setDragOverItem(item.id) } + }} + onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setDragOverItem(null) }} + onDrop={e => { + if (dragItem && dragItemCat === cat && dragItem !== item.id) { + e.preventDefault(); e.stopPropagation() + const ids = items.map(i => i.id) + const fromIdx = ids.indexOf(dragItem) + const toIdx = ids.indexOf(item.id) + ids.splice(fromIdx, 1) + ids.splice(toIdx, 0, dragItem) + reorderBudgetItems(tripId, ids) + setDragItem(null); setDragOverItem(null); setDragItemCat(null) + } + }} + onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'} + onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> + + + + + + + + + + + + ) + })} + {canEdit && handleAddItem(cat, data)} t={t} />} + +
{t('budget.table.name')}{t('budget.table.total')}{t('budget.table.persons')}{t('budget.table.days')}{t('budget.table.perPerson')}{t('budget.table.perDay')}{t('budget.table.perPersonDay')}{t('budget.table.date')}{t('budget.table.note')}
+
+ {canEdit && ( +
{ e.stopPropagation(); e.dataTransfer.effectAllowed = 'move'; setDragItem(item.id); setDragItemCat(cat) }} + onDragEnd={() => { setDragItem(null); setDragOverItem(null); setDragItemCat(null) }} + style={{ cursor: 'grab', display: 'flex', alignItems: 'center', color: 'var(--text-faint)', flexShrink: 0 }}> + +
+ )} +
+ handleUpdateField(item.id, 'name', v)} placeholder={t('budget.table.name')} locale={locale} editTooltip={item.reservation_id ? t('budget.linkedToReservation') : t('budget.editTooltip')} readOnly={!canEdit || !!item.reservation_id} /> + {hasMultipleMembers && ( +
+ setBudgetItemMembers(tripId, item.id, userIds)} + onTogglePaid={(userId, paid) => toggleBudgetMemberPaid(tripId, item.id, userId, paid)} + compact={false} + readOnly={!canEdit} + /> +
+ )} +
+
+
+ handleUpdateField(item.id, 'total_price', v)} style={{ textAlign: 'center' }} placeholder={currencyDecimals(currency) === 0 ? '0' : '0,00'} locale={locale} editTooltip={t('budget.editTooltip')} readOnly={!canEdit} /> + + {hasMultipleMembers ? ( + setBudgetItemMembers(tripId, item.id, userIds)} + onTogglePaid={(userId, paid) => toggleBudgetMemberPaid(tripId, item.id, userId, paid)} + readOnly={!canEdit} + /> + ) : ( + handleUpdateField(item.id, 'persons', v != null ? parseInt(v as string) || null : null)} style={{ textAlign: 'center' }} placeholder="-" locale={locale} editTooltip={t('budget.editTooltip')} readOnly={!canEdit} /> + )} + + handleUpdateField(item.id, 'days', v != null ? parseInt(v as string) || null : null)} style={{ textAlign: 'center' }} placeholder="-" locale={locale} editTooltip={t('budget.editTooltip')} readOnly={!canEdit} /> + {pp != null ? fmt(pp, currency) : '-'}{pd != null ? fmt(pd, currency) : '-'}{ppd != null ? fmt(ppd, currency) : '-'} + {canEdit ? ( +
+ handleUpdateField(item.id, 'expense_date', v || null)} placeholder="—" compact borderless /> +
+ ) : ( + {item.expense_date || '—'} + )} +
handleUpdateField(item.id, 'note', v)} placeholder={t('budget.table.note')} locale={locale} editTooltip={t('budget.editTooltip')} readOnly={!canEdit} /> + {canEdit && ( + + )} +
+
+
+ ) +} diff --git a/client/src/components/Budget/BudgetPanelInlineEditCell.tsx b/client/src/components/Budget/BudgetPanelInlineEditCell.tsx new file mode 100644 index 00000000..c74c8bf5 --- /dev/null +++ b/client/src/components/Budget/BudgetPanelInlineEditCell.tsx @@ -0,0 +1,71 @@ +import { useState, useEffect, useRef } from 'react' + +interface InlineEditCellProps { + value: string | number | null | undefined + onSave: (value: string | number | null) => void + type?: 'text' | 'number' + style?: React.CSSProperties + placeholder?: string + decimals?: number + locale: string + editTooltip?: string + readOnly?: boolean +} + +export default function InlineEditCell({ value, onSave, type = 'text', style = {} as React.CSSProperties, placeholder = '', decimals = 2, locale, editTooltip, readOnly = false }: InlineEditCellProps) { + const [editing, setEditing] = useState(false) + const [editValue, setEditValue] = useState(value ?? '') + const inputRef = useRef(null) + + useEffect(() => { if (editing && inputRef.current) { inputRef.current.focus(); inputRef.current.select() } }, [editing]) + + const save = () => { + setEditing(false) + let v: string | number | null = editValue + if (type === 'number') { const p = parseFloat(String(editValue).replace(',', '.')); v = isNaN(p) ? null : p } + if (v !== value) onSave(v) + } + + const handlePaste = (e: React.ClipboardEvent) => { + if (type !== 'number') return + e.preventDefault() + let text = e.clipboardData.getData('text').trim() + // Strip everything except digits, dots, commas, minus + text = text.replace(/[^\d.,-]/g, '') + // Remove all thousand separators (dots or commas before 3-digit groups), keep last separator as decimal + const lastComma = text.lastIndexOf(',') + const lastDot = text.lastIndexOf('.') + const decimalPos = Math.max(lastComma, lastDot) + if (decimalPos > -1) { + const intPart = text.substring(0, decimalPos).replace(/[.,]/g, '') + const decPart = text.substring(decimalPos + 1) + text = intPart + '.' + decPart + } else { + text = text.replace(/[.,]/g, '') + } + setEditValue(text) + } + + if (editing) { + return setEditValue(e.target.value)} onBlur={save} onPaste={handlePaste} + onKeyDown={e => { if (e.key === 'Enter') save(); if (e.key === 'Escape') { setEditValue(value ?? ''); setEditing(false) } }} + style={{ width: '100%', border: '1px solid var(--accent)', borderRadius: 4, padding: '4px 6px', fontSize: 13, outline: 'none', background: 'var(--bg-input)', color: 'var(--text-primary)', fontFamily: 'inherit', ...style }} + placeholder={placeholder} /> + } + + const display = type === 'number' && value != null + ? Number(value).toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) + : (value || '') + + return ( +
{ if (readOnly) return; setEditValue(value ?? ''); setEditing(true) }} title={readOnly ? undefined : editTooltip} + style={{ cursor: readOnly ? 'default' : 'pointer', padding: '2px 4px', borderRadius: 4, minHeight: 22, display: 'flex', alignItems: 'center', + justifyContent: style?.textAlign === 'center' ? 'center' : 'flex-start', transition: 'background 0.15s', + color: display ? 'var(--text-primary)' : 'var(--text-faint)', fontSize: 13, ...style }} + onMouseEnter={e => { if (!readOnly) e.currentTarget.style.background = 'var(--bg-hover)' }} + onMouseLeave={e => { if (!readOnly) e.currentTarget.style.background = 'transparent' }}> + {display || placeholder || '-'} +
+ ) +} diff --git a/client/src/components/Budget/BudgetPanelMemberChips.tsx b/client/src/components/Budget/BudgetPanelMemberChips.tsx new file mode 100644 index 00000000..c5d0c190 --- /dev/null +++ b/client/src/components/Budget/BudgetPanelMemberChips.tsx @@ -0,0 +1,179 @@ +import ReactDOM from 'react-dom' +import { useState, useEffect, useRef, useCallback } from 'react' +import { Pencil, Users, Check } from 'lucide-react' +import type { BudgetItemMember } from '../../types' + +export interface TripMember { + id: number + username: string + avatar_url?: string | null +} + +// ── Chip with custom tooltip ───────────────────────────────────────────────── +interface ChipWithTooltipProps { + label: string + avatarUrl: string | null + size?: number + paid?: boolean + onClick?: () => void +} + +export function ChipWithTooltip({ label, avatarUrl, size = 20, paid, onClick }: ChipWithTooltipProps) { + const [hover, setHover] = useState(false) + const [pos, setPos] = useState({ top: 0, left: 0 }) + const ref = useRef(null) + + const onEnter = () => { + if (ref.current) { + const rect = ref.current.getBoundingClientRect() + setPos({ top: rect.top - 6, left: rect.left + rect.width / 2 }) + } + setHover(true) + } + + const borderColor = paid ? '#22c55e' : 'var(--border-primary)' + const bg = paid ? 'rgba(34,197,94,0.15)' : 'var(--bg-tertiary)' + + return ( + <> +
setHover(false)} + onClick={onClick} + style={{ + width: size, height: size, borderRadius: '50%', border: `2px solid ${borderColor}`, + background: bg, display: 'flex', alignItems: 'center', justifyContent: 'center', + fontSize: size * 0.4, fontWeight: 700, color: paid ? '#16a34a' : 'var(--text-muted)', + overflow: 'hidden', flexShrink: 0, cursor: onClick ? 'pointer' : 'default', + transition: 'border-color 0.15s, background 0.15s', + }}> + {avatarUrl + ? + : label?.[0]?.toUpperCase() + } +
+ {hover && ReactDOM.createPortal( +
+ {label} + {paid && ( + Paid + )} +
, + document.body + )} + + ) +} + +// ── Budget Member Chips (for Persons column) ──────────────────────────────── +interface BudgetMemberChipsProps { + members?: BudgetItemMember[] + tripMembers?: TripMember[] + onSetMembers: (memberIds: number[]) => void + onTogglePaid?: (userId: number, paid: boolean) => void + compact?: boolean + readOnly?: boolean +} + +export default function BudgetMemberChips({ members = [], tripMembers = [], onSetMembers, onTogglePaid, compact = true, readOnly = false }: BudgetMemberChipsProps) { + const chipSize = compact ? 20 : 30 + const btnSize = compact ? 18 : 28 + const iconSize = compact ? (members.length > 0 ? 8 : 9) : (members.length > 0 ? 12 : 14) + const [showDropdown, setShowDropdown] = useState(false) + const [dropPos, setDropPos] = useState({ top: 0, left: 0 }) + const btnRef = useRef(null) + const dropRef = useRef(null) + + const openDropdown = useCallback(() => { + if (btnRef.current) { + const rect = btnRef.current.getBoundingClientRect() + setDropPos({ top: rect.bottom + 4, left: rect.left + rect.width / 2 }) + } + setShowDropdown(v => !v) + }, []) + + useEffect(() => { + if (!showDropdown) return + const close = (e: MouseEvent) => { + if (dropRef.current && dropRef.current.contains(e.target as Node)) return + if (btnRef.current && btnRef.current.contains(e.target as Node)) return + setShowDropdown(false) + } + document.addEventListener('mousedown', close) + return () => document.removeEventListener('mousedown', close) + }, [showDropdown]) + + const memberIds = members.map(m => m.user_id) + + const toggleMember = (userId: number) => { + const newIds = memberIds.includes(userId) + ? memberIds.filter(id => id !== userId) + : [...memberIds, userId] + onSetMembers(newIds) + } + + return ( +
+ {members.map(m => ( + onTogglePaid(m.user_id, !m.paid) : undefined} + /> + ))} + {!readOnly && ( + + )} + {showDropdown && ReactDOM.createPortal( +
+ {tripMembers.map(tm => { + const isActive = memberIds.includes(tm.id) + return ( + + ) + })} +
, + document.body + )} +
+ ) +} diff --git a/client/src/components/Budget/BudgetPanelPerPersonInline.tsx b/client/src/components/Budget/BudgetPanelPerPersonInline.tsx new file mode 100644 index 00000000..5e248502 --- /dev/null +++ b/client/src/components/Budget/BudgetPanelPerPersonInline.tsx @@ -0,0 +1,64 @@ +import { useState, useEffect } from 'react' +import { budgetApi } from '../../api/client' +import type { BudgetItem } from '../../types' +import { fmtNum, colorForUserId, widgetTheme } from './BudgetPanel.helpers' +import RingAvatar from './BudgetPanelRingAvatar' + +interface PerPersonSummaryEntry { + user_id: number + username: string + avatar_url: string | null + total_assigned: number +} + +interface PerPersonInlineProps { + tripId: number + budgetItems: BudgetItem[] + currency: string + locale: string +} + +export default function PerPersonInline({ tripId, budgetItems, currency, locale, grandTotal, theme }: PerPersonInlineProps & { grandTotal: number; theme: ReturnType }) { + const [data, setData] = useState(null) + const fmt = (v: number) => fmtNum(v, locale, currency) + + useEffect(() => { + budgetApi.perPersonSummary(tripId).then(d => setData(d.summary)).catch(() => {}) + }, [tripId, budgetItems]) + + if (!data || data.length === 0) return null + + const people = data.map(p => ({ ...p, color: colorForUserId(p.user_id) })) + + return ( + <> + {grandTotal > 0 && ( +
+ {people.map(p => ( +
+ ))} +
+ )} + +
+ {people.map(p => { + const percent = grandTotal > 0 ? Math.round((p.total_assigned / grandTotal) * 100) : 0 + return ( +
+ +
+
{p.username}
+
{percent}%
+
+
{fmt(p.total_assigned)}
+
+ ) + })} +
+ + ) +} diff --git a/client/src/components/Budget/BudgetPanelPieChart.tsx b/client/src/components/Budget/BudgetPanelPieChart.tsx new file mode 100644 index 00000000..a72964a8 --- /dev/null +++ b/client/src/components/Budget/BudgetPanelPieChart.tsx @@ -0,0 +1,53 @@ +import { Wallet } from 'lucide-react' + +interface PieSegment { + label: string + value: number + color: string +} + +// ── Pie Chart (pure CSS conic-gradient) ────────────────────────────────────── +interface PieChartProps { + segments: PieSegment[] + size?: number + totalLabel: string +} + +export default function PieChart({ segments, size = 200, totalLabel }: PieChartProps) { + if (!segments.length) return null + + const total = segments.reduce((s, x) => s + x.value, 0) + if (total === 0) return null + + let cumDeg = 0 + const stops = segments.map(seg => { + const start = cumDeg + const deg = (seg.value / total) * 360 + cumDeg += deg + return `${seg.color} ${start}deg ${start + deg}deg` + }).join(', ') + + return ( +
+
+
+ + {totalLabel} +
+
+ ) +} diff --git a/client/src/components/Budget/BudgetPanelRingAvatar.tsx b/client/src/components/Budget/BudgetPanelRingAvatar.tsx new file mode 100644 index 00000000..585bfece --- /dev/null +++ b/client/src/components/Budget/BudgetPanelRingAvatar.tsx @@ -0,0 +1,22 @@ +import { colorForUserId } from './BudgetPanel.helpers' + +export default function RingAvatar({ userId, username, avatarUrl, size = 34, innerBg = '#17171d', textColor = '#fff' }: { userId: number; username?: string; avatarUrl?: string | null; size?: number; innerBg?: string; textColor?: string }) { + const color = colorForUserId(userId) + return ( +
+
+ {avatarUrl ? : username?.[0]?.toUpperCase()} +
+
+ ) +} diff --git a/client/src/components/Budget/BudgetPanelSummary.tsx b/client/src/components/Budget/BudgetPanelSummary.tsx new file mode 100644 index 00000000..693ea986 --- /dev/null +++ b/client/src/components/Budget/BudgetPanelSummary.tsx @@ -0,0 +1,280 @@ +import type { Dispatch, SetStateAction } from 'react' +import { Wallet, Info, ChevronDown, ChevronRight, TrendingUp, TrendingDown, PieChart as PieChartIcon } from 'lucide-react' +import type { BudgetItem } from '../../types' +import { currencyDecimals } from '../../utils/formatters' +import { SYMBOLS } from './BudgetPanel.constants' +import { hexLighten, widgetTheme } from './BudgetPanel.helpers' +import RingAvatar from './BudgetPanelRingAvatar' +import PerPersonInline from './BudgetPanelPerPersonInline' +import type { SettlementData, PieSegment } from './useBudgetPanel' + +interface BudgetSummaryProps { + theme: ReturnType + currency: string + locale: string + grandTotal: number + hasMultipleMembers: boolean + budgetItems: BudgetItem[] + settlement: SettlementData | null + settlementOpen: boolean + setSettlementOpen: Dispatch> + pieSegments: PieSegment[] + isDark: boolean + tripId: number + t: (key: string) => string + fmt: (v: number | null | undefined, cur: string) => string +} + +export default function BudgetSummary({ theme, currency, locale, grandTotal, hasMultipleMembers, budgetItems, + settlement, settlementOpen, setSettlementOpen, pieSegments, isDark, tripId, t, fmt }: BudgetSummaryProps) { + return ( +
+ +
+
+
+ +
+
+
{t('budget.totalBudget')}
+
+
+ + {(() => { + const decimals = currencyDecimals(currency) + const full = Number(grandTotal).toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) + const sep = (0.1).toLocaleString(locale).replace(/\d/g, '') + const [integerPart, decimalPart] = decimals > 0 ? full.split(sep) : [full, ''] + return ( +
+ {integerPart} + {decimalPart && {sep}{decimalPart}} + {SYMBOLS[currency] || currency} +
+ ) + })()} +
+ {currency} +
+ + {hasMultipleMembers && (budgetItems || []).some(i => (i.members?.length ?? 0) > 0) && ( + + )} + + {/* Settlement dropdown inside the total card */} + {hasMultipleMembers && settlement && settlement.flows.length > 0 && ( +
+ + + {settlementOpen && ( +
+ {settlement.flows.map((flow, i) => ( +
{ e.currentTarget.style.background = theme.flowHoverBg; e.currentTarget.style.borderColor = theme.flowHoverBorder }} + onMouseLeave={e => { e.currentTarget.style.background = theme.flowBg; e.currentTarget.style.borderColor = theme.flowBorder }} + > + +
+ + {fmt(flow.amount, currency)} + +
+
+
+
+ +
+ ))} + + {settlement.balances.filter(b => Math.abs(b.balance) > 0.01).length > 0 && ( +
+
+ {t('budget.netBalances')} +
+
+ {settlement.balances.filter(b => Math.abs(b.balance) > 0.01).map(b => { + const positive = b.balance > 0 + const Trend = positive ? TrendingUp : TrendingDown + return ( +
+ + + {b.username} + + + + {positive ? '+' : ''}{fmt(b.balance, currency)} + +
+ ) + })} +
+
+ )} +
+ )} +
+ )} +
+ + {pieSegments.length > 0 && (() => { + const decimals = currencyDecimals(currency) + const total = pieSegments.reduce((s, x) => s + x.value, 0) + const totalFmt = Number(total).toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) + const decimalSep = (0.1).toLocaleString(locale).replace(/\d/g, '') + const [totalInt, totalDec] = decimals > 0 ? totalFmt.split(decimalSep) : [totalFmt, ''] + const R = 80 + const CIRC = 2 * Math.PI * R + let dashOffset = 0 + return ( +
+
+
+ +
+
+
{t('budget.byCategory')}
+
+
+ +
+ + + {pieSegments.map((seg, i) => { + const c2 = hexLighten(seg.color, 0.2) + return ( + + + + + ) + })} + + + {pieSegments.map((seg, i) => { + const segLen = total > 0 ? (seg.value / total) * CIRC : 0 + const circle = ( + + ) + dashOffset += segLen + return circle + })} + +
+
{t('budget.total')}
+
+ {totalInt} + {totalDec && {decimalSep}{totalDec}} +
+
{currency}
+
+
+ +
+ {pieSegments.map((seg, i) => { + const pct = total > 0 ? (seg.value / total) * 100 : 0 + const pctLabel = pct.toFixed(1).replace('.', decimalSep) + '%' + const c2 = hexLighten(seg.color, 0.2) + const chipColor = isDark ? hexLighten(seg.color, 0.35) : seg.color + return ( +
e.currentTarget.style.background = theme.rowHover} + onMouseLeave={e => e.currentTarget.style.background = 'transparent'} + > +
+
+
{seg.name}
+
{fmt(seg.value, currency)}
+
+ {pctLabel} +
+ ) + })} +
+
+ ) + })()} + +
+ ) +} diff --git a/client/src/components/Budget/CostsPanel.tsx b/client/src/components/Budget/CostsPanel.tsx new file mode 100644 index 00000000..8663bedb --- /dev/null +++ b/client/src/components/Budget/CostsPanel.tsx @@ -0,0 +1,814 @@ +import { useState, useEffect, useMemo, useCallback } from 'react' +import { useSearchParams } from 'react-router-dom' +import { ArrowDown, ArrowUp, BarChart3, Plus, Search, ArrowRight, Check, RotateCcw, History, Pencil, Trash2 } from 'lucide-react' +import { useTripStore } from '../../store/tripStore' +import { useAuthStore } from '../../store/authStore' +import { useSettingsStore } from '../../store/settingsStore' +import { useCanDo } from '../../store/permissionsStore' +import { useToast } from '../shared/Toast' +import { useTranslation } from '../../i18n' +import { budgetApi } from '../../api/client' +import { useExchangeRates } from '../../hooks/useExchangeRates' +import { useIsMobile } from '../../hooks/useIsMobile' +import { formatMoney, currencyDecimals, currencyLocale } from '../../utils/formatters' +import Modal from '../shared/Modal' +import CustomSelect from '../shared/CustomSelect' +import { CustomDatePicker } from '../shared/CustomDateTimePicker' +import { SYMBOLS, CURRENCIES, SPLIT_COLORS } from './BudgetPanel.constants' +import { COST_CATEGORY_LIST, catMeta } from './costsCategories' +import type { BudgetItem } from '../../types' +import type { TripMember } from './BudgetPanelMemberChips' + +interface CostsPanelProps { + tripId: number + tripMembers?: TripMember[] +} + +interface Settlement { + id: number + from_user_id: number + to_user_id: number + amount: number + created_at?: string + from_username?: string + to_username?: string +} +interface SettlementData { + balances: { user_id: number; username: string; avatar_url: string | null; balance: number }[] + flows: { from: { user_id: number; username: string }; to: { user_id: number; username: string }; amount: number }[] + settlements: Settlement[] +} + +const round2 = (n: number) => Math.round(n * 100) / 100 +const FIELD_H = 40 // shared height for the amount / currency / day row in the modal + +export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps) { + const { trip, budgetItems, deleteBudgetItem, loadBudgetItems } = useTripStore() + const me = useAuthStore(s => s.user?.id ?? -1) + const can = useCanDo() + const canEdit = can('budget_edit', trip) + const toast = useToast() + const { t, locale } = useTranslation() + const isMobile = useIsMobile() + + // Display/base currency = the user's preferred currency (Settings), falling back + // to the trip's own currency. Everything in Costs is converted to and shown in it. + const displayCurrency = useSettingsStore(s => s.settings.default_currency) + const base = (displayCurrency || trip?.currency || 'EUR').toUpperCase() + // Pre-rework rows stored currency = NULL, meaning "the trip's own currency". + const tripCurrency = (trip?.currency || base).toUpperCase() + const { convert } = useExchangeRates(base) + const curOf = useCallback((e: BudgetItem) => (e.currency || tripCurrency), [tripCurrency]) + const [settlement, setSettlement] = useState(null) + const [filter, setFilter] = useState<'all' | 'mine' | 'owed'>('all') + const [search, setSearch] = useState('') + const [histOpen, setHistOpen] = useState(false) + const [modalOpen, setModalOpen] = useState(false) + const [editing, setEditing] = useState(null) + + const people = tripMembers + const personById = useCallback((id: number) => people.find(p => p.id === id), [people]) + const personName = useCallback((id: number) => id === me ? t('costs.you') : (personById(id)?.username || '?'), [me, personById, t]) + const colorFor = useCallback((id: number) => { + const idx = people.findIndex(p => p.id === id) + return SPLIT_COLORS[(idx >= 0 ? idx : 0) % SPLIT_COLORS.length].gradient + }, [people]) + const initial = useCallback((id: number) => id === me ? t('costs.youShort') : (personById(id)?.username || '?').charAt(0).toUpperCase(), [me, personById, t]) + + const fmt = useCallback((v: number, c = base) => formatMoney(v, c, locale), [base, locale]) + const fmt0 = useCallback((v: number, c = base) => formatMoney(v, c, locale, { decimals: 0 }), [base, locale]) + + const loadSettlement = useCallback(() => { + budgetApi.settlement(tripId, base).then(setSettlement).catch(() => {}) + }, [tripId, base]) + + useEffect(() => { loadBudgetItems(tripId); loadSettlement() }, [tripId]) + useEffect(() => { loadSettlement() }, [budgetItems.length, base]) + + // The bottom-nav "+" on the Costs tab opens the add-expense modal via ?create=expense. + const [searchParams, setSearchParams] = useSearchParams() + useEffect(() => { + if (searchParams.get('create') === 'expense') { + setEditing(null); setModalOpen(true) + setSearchParams(p => { p.delete('create'); return p }, { replace: true }) + } + }, [searchParams]) + + // ── derived expense maths (everything converted to the base currency) ──── + const baseTotal = (e: BudgetItem) => convert(e.total_price || 0, curOf(e)) + const myPaidOf = (e: BudgetItem) => (e.payers || []).filter(p => p.user_id === me).reduce((a, p) => a + convert(p.amount, curOf(e)), 0) + const myShareOf = (e: BudgetItem) => { + const n = (e.members || []).length + if (!n || !(e.members || []).some(m => m.user_id === me)) return 0 + return baseTotal(e) / n + } + + const totals = useMemo(() => { + const totalSpend = budgetItems.reduce((a, e) => a + baseTotal(e), 0) + const myPaid = budgetItems.reduce((a, e) => a + myPaidOf(e), 0) + const myShare = budgetItems.reduce((a, e) => a + myShareOf(e), 0) + const owe = (settlement?.flows || []).filter(f => f.from.user_id === me).reduce((a, f) => a + f.amount, 0) + const owed = (settlement?.flows || []).filter(f => f.to.user_id === me).reduce((a, f) => a + f.amount, 0) + return { totalSpend, myPaid, myShare, owe, owed } + }, [budgetItems, settlement, me]) + + // ── filtering + day grouping ──────────────────────────────────────────── + const filtered = useMemo(() => { + let list = budgetItems.slice() + if (filter === 'mine') list = list.filter(e => myPaidOf(e) > 0) + if (filter === 'owed') list = list.filter(e => round2(myPaidOf(e) - myShareOf(e)) > 0) + const q = search.trim().toLowerCase() + if (q) list = list.filter(e => e.name.toLowerCase().includes(q)) + return list + }, [budgetItems, filter, search, me]) + + const dayGroups = useMemo(() => { + const groups: { day: string; items: BudgetItem[] }[] = [] + const labelOf = (e: BudgetItem) => { + if (!e.expense_date) return t('costs.noDate') + try { return new Date(e.expense_date + 'T00:00:00Z').toLocaleDateString(locale, { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' }) } catch { return e.expense_date } + } + const sorted = filtered.slice().sort((a, b) => (b.expense_date || '').localeCompare(a.expense_date || '')) + for (const e of sorted) { + const day = labelOf(e) + let g = groups.find(x => x.day === day) + if (!g) { g = { day, items: [] }; groups.push(g) } + g.items.push(e) + } + return groups + }, [filtered, locale, t]) + + // ── settle actions ────────────────────────────────────────────────────── + const settleFlow = async (fromId: number, toId: number, amount: number) => { + try { + await budgetApi.createSettlement(tripId, { from_user_id: fromId, to_user_id: toId, amount }) + loadSettlement() + } catch { toast.error(t('common.unknownError')) } + } + const undoSettlement = async (id: number) => { + try { await budgetApi.deleteSettlement(tripId, id); loadSettlement() } catch { toast.error(t('common.unknownError')) } + } + const settleAll = async () => { + const flows = settlement?.flows || [] + if (!flows.length) return + try { + for (const f of flows) await budgetApi.createSettlement(tripId, { from_user_id: f.from.user_id, to_user_id: f.to.user_id, amount: f.amount }) + loadSettlement() + } catch { toast.error(t('common.unknownError')) } + } + + const dateMeta = useMemo(() => { + if (!trip?.start_date || !trip?.end_date) return null + try { + const s = new Date(trip.start_date + 'T00:00:00Z'), e = new Date(trip.end_date + 'T00:00:00Z') + const days = Math.round((e.getTime() - s.getTime()) / 86400000) + 1 + const opt = { day: 'numeric', month: 'short', timeZone: 'UTC' } as const + return { range: `${s.toLocaleDateString(locale, opt)} – ${e.toLocaleDateString(locale, opt)}`, days } + } catch { return null } + }, [trip?.start_date, trip?.end_date, locale]) + + const handleDelete = async (id: number) => { + try { await deleteBudgetItem(tripId, id); loadSettlement() } catch { toast.error(t('common.unknownError')) } + } + + // ── small presentational helpers ──────────────────────────────────────── + const Avatar = ({ id, size = 24 }: { id: number; size?: number }) => { + const url = personById(id)?.avatar_url + if (url) return + return {initial(id)} + } + + const cardCls = 'bg-surface-card border border-edge' + const labelCls = 'text-[11px] font-semibold uppercase tracking-[0.12em] text-content-faint' + + // Big money number with the design's muted symbol/decimals, locale-correct via Intl. + const bigMoney = (amount: number, smallSize: number, mutedColor: string) => { + let parts: Intl.NumberFormatPart[] | null = null + try { + const d = currencyDecimals(base) + parts = new Intl.NumberFormat(currencyLocale(base), { style: 'currency', currency: base, minimumFractionDigits: d, maximumFractionDigits: d }).formatToParts(amount || 0) + } catch { return <>{formatMoney(amount, base, locale)} } + const isBig = (p: Intl.NumberFormatPart) => p.type === 'integer' || p.type === 'group' || p.type === 'minusSign' + return <>{parts.map((p, i) => {p.value})} + } + + return ( +
+ {isMobile ? : ( +
+ {/* ── Header bar ── */} +
+
+ {dateMeta && ( + + {dateMeta.range} · {t('costs.daysCount', { count: dateMeta.days })} + + )} + + + {people.slice(0, 4).map((p, i) => { + const common = { width: 22, height: 22, borderRadius: '50%', border: '2px solid var(--bg-card)', marginLeft: i ? -8 : 0, flexShrink: 0 } as const + return p.avatar_url + ? + : {(p.id === me ? t('costs.youShort') : p.username.charAt(0)).toUpperCase()} + })} + + {t('costs.travelers', { count: people.length })} + +
+ {canEdit && ( +
+ + +
+ )} +
+ + {/* ── Summary cards ── */} +
+ } tone="owe" + foot={totals.owe > 0.01 + ? f.from.user_id === me).map(f => f.to.user_id)} lead={t('costs.to')} Avatar={Avatar} name={personName} /> + : {t('costs.allSettled')}} /> + } tone="owed" + foot={totals.owed > 0.01 + ? f.to.user_id === me).map(f => f.from.user_id)} lead={t('costs.from')} Avatar={Avatar} name={personName} /> + : {t('costs.nothingOwed')}} /> + } tone="total" + foot={{t('costs.yourShare')} · {fmt0(totals.myShare)}{t('costs.youPaid')} · {fmt0(totals.myPaid)}} /> +
+ + {/* ── Main grid ── */} +
+ {/* expenses */} +
+
+

+ {t('costs.expenses')} +

+
+
+ + setSearch(e.target.value)} placeholder={t('costs.searchPlaceholder')} + className="text-content" style={{ border: 0, background: 'none', outline: 'none', fontSize: 13, width: 150, fontFamily: 'inherit' }} /> +
+
+ {(['all', 'mine', 'owed'] as const).map(f => ( + + ))} +
+
+
+ + {dayGroups.length === 0 ? ( +
+ {search ? t('costs.noMatch') : t('costs.emptyText')} +
+ ) : dayGroups.map(g => { + const dtot = g.items.reduce((a, e) => a + baseTotal(e), 0) + return ( +
+
+ {g.day}{t('costs.spent', { amount: fmt(dtot) })} +
+
+ {g.items.map(e => )} +
+
+ ) + })} +
+ + {/* sidebar */} +
+ {/* settle up */} +
+
+
{t('costs.settleUp')} · {(settlement?.flows || []).length}
+ +
+ +
+ + {/* balances */} +
+
{t('costs.balances')}
+ +
+ + {/* by category */} +
+
{t('costs.byCategory')}
+ +
+
+
+
)} + + {modalOpen && ( + setModalOpen(false)} + onSaved={() => { setModalOpen(false); loadBudgetItems(tripId); loadSettlement() }} /> + )} + + setHistOpen(false)} title={t('costs.settleHistory')} size="md"> + + + + +
+ ) + + // ── shared settle-flow list ────────────────────────────────────────────── + function SettleFlows() { + const flows = settlement?.flows || [] + if (flows.length === 0) return ( +
+
+
{t('costs.everyoneSquare')}
+
{t('costs.nothingOutstanding')}
+
+ ) + return ( +
+ {flows.map((f, i) => ( +
+
+ +
+
+ {fmt(f.amount)} + {canEdit && } +
+
+ ))} +
+ ) + } + + // ── mobile layout (Budget1Mobile.html): single flat column, total card on top ── + function MobileBody() { + return ( +
+ {/* Total card */} +
+
{t('costs.totalSpend')}
+
{bigMoney(totals.totalSpend, 24, 'rgba(255,255,255,0.6)')}
+
+ {t('costs.yourShare')} · {fmt0(totals.myShare)} + {t('costs.youPaid')} · {fmt0(totals.myPaid)} +
+ {canEdit && ( + + )} +
+ + {/* Owe / Owed */} +
+
+
+
{t('costs.youOwe')}
+
{t('costs.youOweSub')}
+
{bigMoney(totals.owe, 16, 'var(--c-ink3)')}
+
+
+
+
{t('costs.youreOwed')}
+
{t('costs.youreOwedSub')}
+
{bigMoney(totals.owed, 16, 'var(--c-ink3)')}
+
+
+ + {/* Settle up */} +
+
+
{t('costs.settleUp')} {(settlement?.flows || []).length}
+ +
+ +
+ + {/* Expenses */} +
+
{t('costs.expenses')}
+
+ + setSearch(e.target.value)} placeholder={t('costs.searchPlaceholder')} className="text-content" style={{ border: 0, background: 'none', outline: 'none', fontSize: 14, width: '100%', fontFamily: 'inherit' }} /> +
+
+ {(['all', 'mine', 'owed'] as const).map(f => ( + + ))} +
+ {dayGroups.length === 0 + ?
{search ? t('costs.noMatch') : t('costs.emptyText')}
+ : dayGroups.map(g => { + const dtot = g.items.reduce((a, e) => a + baseTotal(e), 0) + return ( +
+
{g.day}{t('costs.spent', { amount: fmt(dtot) })}
+
{g.items.map(e => )}
+
+ ) + })} +
+ + {/* Balances */} +
+
{t('costs.balances')}
+ +
+ + {/* By category */} +
+
{t('costs.byCategory')}
+ +
+
+ ) + } + + // ── inline subcomponents (close over helpers) ──────────────────────────── + function ExpenseRow({ e }: { e: BudgetItem }) { + const c = catMeta(e.category) + const Icon = c.Icon + const cur = curOf(e) + const payers = (e.payers || []).filter(p => p.amount > 0) + const net = round2(myPaidOf(e) - myShareOf(e)) + return ( +
+ +
+
{e.name}
+ {payers.length > 0 && ( +
+ {payers.map(p => ( + + + {fmt(convert(p.amount, cur))} + + ))} +
+ )} + {!isMobile && ( +
+ {t(c.labelKey)}{cur !== base ? ` · ${fmt(e.total_price, cur)} → ${fmt(baseTotal(e))}` : ''} +
+ )} +
+
+
+
{fmt(baseTotal(e))}
+ {(e.members || []).length > 0 && Math.abs(net) > 0.01 && ( +
0 ? '#16a34a' : '#dc2626' }}> + {net > 0 ? t('costs.youLent', { amount: fmt(net) }) : t('costs.youBorrowed', { amount: fmt(-net) })} +
+ )} +
+ {canEdit && ( +
+ + +
+ )} +
+
+ ) + } + + function BalancesList({ balances }: { balances: SettlementData['balances'] }) { + const rows = people.map(p => balances.find(b => b.user_id === p.id) || { user_id: p.id, username: p.username, avatar_url: null, balance: 0 }) + const max = Math.max(1, ...rows.map(r => Math.abs(r.balance))) + return ( +
+ {rows.map(r => { + const pct = Math.min(100, Math.abs(r.balance) / max * 100) + const pos = r.balance > 0.01, neg = r.balance < -0.01 + return ( +
+ +
+
{personName(r.user_id)}
+
+ + {pos && } + {neg && } +
+
+
+ {pos ? '+' + fmt(r.balance) : neg ? '−' + fmt(-r.balance) : fmt(0)} +
+
+ ) + })} +
+ ) + } + + function CategoryBreakdown() { + const tot: Record = {} + let grand = 0 + for (const e of budgetItems) { const k = catMeta(e.category).key; tot[k] = (tot[k] || 0) + baseTotal(e); grand += baseTotal(e) } + const rows = COST_CATEGORY_LIST.filter(c => (tot[c.key] || 0) > 0).sort((a, b) => (tot[b.key] || 0) - (tot[a.key] || 0)) + if (rows.length === 0) return
{t('costs.noCategories')}
+ return ( +
+ {rows.map(c => { + const v = tot[c.key]; const pct = grand ? v / grand * 100 : 0 + return ( +
+ + {t(c.labelKey)} + {fmt0(v)} +
+ +
+
+ ) + })} +
+ ) + } +} + +// ── pure subcomponents ───────────────────────────────────────────────────── +function SummaryCard({ label, sub, amount, currency, locale, icon, foot, tone }: { label: string; sub: string; amount: number; currency: string; locale: string; icon: React.ReactNode; foot: React.ReactNode; tone: 'owe' | 'owed' | 'total' }) { + const total = tone === 'total' + const accent = tone === 'owe' ? '#dc2626' : tone === 'owed' ? '#16a34a' : undefined + const muted = total ? 'rgba(255,255,255,0.55)' : 'var(--text-faint)' + // formatToParts keeps the design's "big integer + muted symbol/decimals" styling + // while letting Intl place the symbol and pick separators per locale + currency. + let parts: Intl.NumberFormatPart[] | null = null + try { + const d = currencyDecimals(currency) + parts = new Intl.NumberFormat(currencyLocale(currency), { style: 'currency', currency: (currency || 'EUR').toUpperCase(), minimumFractionDigits: d, maximumFractionDigits: d }).formatToParts(amount || 0) + } catch { parts = null } + const big = (p: Intl.NumberFormatPart) => p.type === 'integer' || p.type === 'group' || p.type === 'minusSign' + return ( +
+
+ {icon} +
+
{label}
+
{sub}
+
+
+
+ {parts + ? parts.map((p, i) => {p.value}) + : {formatMoney(amount, currency, locale)}} +
+
{foot}
+
+ ) +} + +function FlowPills({ ids, lead, Avatar, name }: { ids: number[]; lead: string; Avatar: (p: { id: number; size?: number }) => React.JSX.Element; name: (id: number) => string }) { + const uniq = Array.from(new Set(ids)) + return ( + + {lead} + {uniq.map(id => ( + + {name(id)} + + ))} + + ) +} + +function SettleHistory({ settlements, fmt, Avatar, name, onUndo, canEdit }: { + settlements: Settlement[]; fmt: (v: number) => string; Avatar: (p: { id: number; size?: number }) => React.JSX.Element; name: (id: number) => string; onUndo: (id: number) => void; canEdit: boolean +}) { + const { t } = useTranslation() + if (settlements.length === 0) return
{t('costs.noSettlements')}
+ const total = settlements.reduce((a, s) => a + s.amount, 0) + return ( +
+
+ {t('costs.paymentsSettled', { count: settlements.length })}{fmt(total)} +
+
+ {settlements.map(s => ( +
+
+ +
+
+ {fmt(s.amount)} + {canEdit && } +
+
+ ))} +
+
+ ) +} + +// ── Add / edit expense modal ─────────────────────────────────────────────── +function ExpenseModal({ tripId, base, people, me, editing, onClose, onSaved }: { + tripId: number; base: string; people: TripMember[]; me: number; editing: BudgetItem | null; onClose: () => void; onSaved: () => void +}) { + const { t, locale } = useTranslation() + const toast = useToast() + const { addBudgetItem, updateBudgetItem } = useTripStore() + const { convert } = useExchangeRates(base) + const sym = (c: string) => SYMBOLS[c] || (c + ' ') + + const [name, setName] = useState(editing?.name || '') + const [cat, setCat] = useState(editing ? catMeta(editing.category).key : 'food') + const [currency, setCurrency] = useState((editing?.currency || base).toUpperCase()) + const [day, setDay] = useState(editing?.expense_date || new Date().toISOString().slice(0, 10)) + const [payers, setPayers] = useState>(() => { + const m: Record = {} + for (const p of editing?.payers || []) m[p.user_id] = String(p.amount) + return m + }) + const [split, setSplit] = useState>(() => + editing ? new Set((editing.members || []).map(m => m.user_id)) : new Set(people.map(p => p.id))) + const [saving, setSaving] = useState(false) + + const payersTotal = Object.values(payers).reduce((a, v) => a + (parseFloat(v) || 0), 0) + const each = split.size > 0 ? payersTotal / split.size : 0 + const valid = name.trim().length > 0 && split.size > 0 && payersTotal > 0 + + const save = async () => { + if (!valid) return + setSaving(true) + const payerList = Object.entries(payers).map(([uid, v]) => ({ user_id: Number(uid), amount: parseFloat(v) || 0 })).filter(p => p.amount > 0) + const data = { + name: name.trim(), category: cat, + // Store the actual currency the amounts were entered in; conversion to the + // viewer's display currency happens live (real rates), no manual rate. + currency, + payers: payerList, member_ids: [...split], + expense_date: day || null, + } + try { + if (editing) await updateBudgetItem(tripId, editing.id, data) + else await addBudgetItem(tripId, data) + onSaved() + } catch { toast.error(t('common.unknownError')) } finally { setSaving(false) } + } + + const inputCls = 'w-full bg-surface-input border border-edge text-content' + const labelCls = 'block text-[11px] font-semibold uppercase tracking-[0.08em] text-content-faint mb-[6px]' + + return ( + + + +
+ }> +
+
+ + setName(e.target.value)} placeholder={t('costs.namePlaceholder')} className={inputCls} style={{ borderRadius: 10, padding: '11px 13px', fontSize: 14, outline: 'none' }} /> +
+ +
+ +
+ {sym(currency)} + {payersTotal.toFixed(2)} +
+
+
+
+ + setCurrency(String(v))} searchable + options={CURRENCIES.map(c => ({ value: c, label: SYMBOLS[c] ? `${c} ${SYMBOLS[c]}` : c }))} + style={{ width: '100%' }} /> +
+
+ + +
+
+ + {currency !== base && payersTotal > 0 && ( +
+ {formatMoney(payersTotal, currency, locale)} + + {formatMoney(convert(payersTotal, currency), base, locale)} + · {t('costs.liveRate')} +
+ )} + +
+ +
+ {COST_CATEGORY_LIST.map(c => { + const Icon = c.Icon; const on = cat === c.key + return ( + + ) + })} +
+
+ +
+ +
+ {people.map(p => ( +
+ {p.id === me ? t('costs.you') : p.username} +
+ {sym(currency)} + setPayers(prev => ({ ...prev, [p.id]: e.target.value }))} + className="text-content" style={{ width: '100%', border: 0, background: 'none', outline: 'none', fontSize: 14, fontWeight: 600, padding: '8px 0', textAlign: 'right' }} /> +
+
+ ))} +
+
+ +
+ +
+ {people.map(p => { + const on = split.has(p.id) + return ( + + ) + })} +
+
+ {split.size === 0 ? t('costs.pickSomeone') : t('costs.splitSummary', { count: split.size, amount: sym(currency) + each.toFixed(2) })} +
+
+
+ + ) +} diff --git a/client/src/components/Budget/costsCategories.tsx b/client/src/components/Budget/costsCategories.tsx new file mode 100644 index 00000000..1a6a619f --- /dev/null +++ b/client/src/components/Budget/costsCategories.tsx @@ -0,0 +1,39 @@ +import { Hotel, Utensils, ShoppingCart, Bus, Plane, Ticket, Camera, ShoppingBag, FileText, HeartPulse, Coins, MoreHorizontal } from 'lucide-react' +import type { LucideIcon } from 'lucide-react' +import { COST_CATEGORIES, type CostCategory } from '@trek/shared' + +/** + * The fixed Costs categories. Users can't add their own — every expense maps to + * one of these. Category colour is the one place an accent is allowed (it + * visualises the category); everything else stays black/white. The label comes + * from i18n (`costs.cat.*`). + */ +export interface CostCategoryMeta { + key: CostCategory + labelKey: string + Icon: LucideIcon + color: string +} + +export const COST_CAT_META: Record = { + accommodation: { key: 'accommodation', labelKey: 'costs.cat.accommodation', Icon: Hotel, color: '#16a34a' }, + food: { key: 'food', labelKey: 'costs.cat.food', Icon: Utensils, color: '#ea580c' }, + groceries: { key: 'groceries', labelKey: 'costs.cat.groceries', Icon: ShoppingCart, color: '#65a30d' }, + transport: { key: 'transport', labelKey: 'costs.cat.transport', Icon: Bus, color: '#2563eb' }, + flights: { key: 'flights', labelKey: 'costs.cat.flights', Icon: Plane, color: '#0ea5e9' }, + activities: { key: 'activities', labelKey: 'costs.cat.activities', Icon: Ticket, color: '#9333ea' }, + sightseeing: { key: 'sightseeing', labelKey: 'costs.cat.sightseeing', Icon: Camera, color: '#db2777' }, + shopping: { key: 'shopping', labelKey: 'costs.cat.shopping', Icon: ShoppingBag, color: '#e11d48' }, + fees: { key: 'fees', labelKey: 'costs.cat.fees', Icon: FileText, color: '#475569' }, + health: { key: 'health', labelKey: 'costs.cat.health', Icon: HeartPulse, color: '#dc2626' }, + tips: { key: 'tips', labelKey: 'costs.cat.tips', Icon: Coins, color: '#d97706' }, + other: { key: 'other', labelKey: 'costs.cat.other', Icon: MoreHorizontal, color: '#6b7280' }, +} + +export const COST_CATEGORY_LIST: CostCategoryMeta[] = COST_CATEGORIES.map(k => COST_CAT_META[k]) + +/** Map any stored category (incl. legacy free-text values) to a known meta. */ +export function catMeta(cat: string | null | undefined): CostCategoryMeta { + if (cat && cat in COST_CAT_META) return COST_CAT_META[cat as CostCategory] + return COST_CAT_META.other +} diff --git a/client/src/components/Budget/useBudgetPanel.ts b/client/src/components/Budget/useBudgetPanel.ts new file mode 100644 index 00000000..442bf037 --- /dev/null +++ b/client/src/components/Budget/useBudgetPanel.ts @@ -0,0 +1,211 @@ +import { useState, useEffect, useRef, useMemo, useCallback } from 'react' +import type { CSSProperties } from 'react' +import { useTripStore } from '../../store/tripStore' +import { useCanDo } from '../../store/permissionsStore' +import { useToast } from '../shared/Toast' +import { useTranslation } from '../../i18n' +import { budgetApi } from '../../api/client' +import type { BudgetItem } from '../../types' +import { currencyDecimals } from '../../utils/formatters' +import { widgetTheme, fmtNum, calcPP, calcPD, calcPPD } from './BudgetPanel.helpers' +import { PIE_COLORS } from './BudgetPanel.constants' +import type { TripMember } from './BudgetPanelMemberChips' + +function useIsDark(): boolean { + const [dark, setDark] = useState(() => typeof document !== 'undefined' && document.documentElement.classList.contains('dark')) + useEffect(() => { + if (typeof document === 'undefined') return + const mo = new MutationObserver(() => setDark(document.documentElement.classList.contains('dark'))) + mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }) + return () => mo.disconnect() + }, []) + return dark +} + +export interface EditingCat { + name: string + value: string +} + +interface SettlementPerson { + user_id: number + username: string + avatar_url: string | null +} + +interface SettlementFlow { + from: SettlementPerson + to: SettlementPerson + amount: number +} + +interface SettlementBalance { + user_id: number + username: string + avatar_url: string | null + balance: number +} + +export interface SettlementData { + balances: SettlementBalance[] + flows: SettlementFlow[] +} + +export interface PieSegment { + name: string + value: number + color: string +} + +export interface AddItemData { + name: string + total_price: number + persons: number | null + days: number | null + note: string | null + expense_date: string | null +} + +export function useBudgetPanel(tripId: number, tripMembers: TripMember[]) { + const { trip, budgetItems, addBudgetItem, updateBudgetItem, deleteBudgetItem, loadBudgetItems, updateTrip, setBudgetItemMembers, toggleBudgetMemberPaid, reorderBudgetItems, reorderBudgetCategories } = useTripStore() + const can = useCanDo() + const toast = useToast() + const { t, locale } = useTranslation() + const isDark = useIsDark() + const theme = useMemo(() => widgetTheme(isDark), [isDark]) + const [newCategoryName, setNewCategoryName] = useState('') + const [editingCat, setEditingCat] = useState(null) // { name, value } + const [settlement, setSettlement] = useState(null) + const [settlementOpen, setSettlementOpen] = useState(false) + const currency = trip?.currency || 'EUR' + const canEdit = can('budget_edit', trip) + + const fmt = (v: number | null | undefined, cur: string) => fmtNum(v, locale, cur) + const hasMultipleMembers = tripMembers.length > 1 + + // Drag state for categories + const [dragCat, setDragCat] = useState(null) + const [dragOverCat, setDragOverCat] = useState(null) + // Drag state for items within a category + const [dragItem, setDragItem] = useState(null) + const [dragOverItem, setDragOverItem] = useState(null) + const [dragItemCat, setDragItemCat] = useState(null) + + // Load settlement data whenever budget items change + useEffect(() => { + if (!hasMultipleMembers) return + budgetApi.settlement(tripId).then(setSettlement).catch(() => {}) + }, [tripId, budgetItems, hasMultipleMembers]) + + const setCurrency = (cur: string) => { + if (tripId) updateTrip(tripId, { currency: cur }) + } + + useEffect(() => { if (tripId) loadBudgetItems(tripId) }, [tripId]) + + const grouped = useMemo(() => { + const map = new Map() + for (const item of (budgetItems || [])) { + const cat = item.category || 'Other' + if (!map.has(cat)) map.set(cat, []) + map.get(cat)!.push(item) + } + return map + }, [budgetItems]) + + const categoryNames = Array.from(grouped.keys()) + + // Stable color mapping: assign index-based colors once, never reassign on reorder + const colorMapRef = useRef(new Map()) + const categoryColor = useCallback((cat: string) => { + const map = colorMapRef.current + if (!map.has(cat)) { + map.set(cat, PIE_COLORS[map.size % PIE_COLORS.length]) + } + return map.get(cat)! + }, []) + const grandTotal = (budgetItems || []).reduce((s, i) => s + (i.total_price || 0), 0) + + const pieSegments = useMemo(() => + categoryNames.map((cat, i) => ({ + name: cat, + value: (grouped.get(cat) || []).reduce((s, x) => s + (x.total_price || 0), 0), + color: categoryColor(cat), + })).filter(s => s.value > 0) + , [grouped, categoryNames]) + + const handleAddItem = async (category: string, data: AddItemData) => { try { await addBudgetItem(tripId, { ...data, category }) } catch { toast.error(t('common.error')) } } + const handleUpdateField = async (id: number, field: string, value: unknown) => { try { await updateBudgetItem(tripId, id, { [field]: value } as Partial) } catch { toast.error(t('common.error')) } } + const handleDeleteItem = async (id: number) => { try { await deleteBudgetItem(tripId, id) } catch { toast.error(t('common.error')) } } + const handleDeleteCategory = async (cat: string) => { + const items = grouped.get(cat) || [] + try { for (const item of Array.from(items)) await deleteBudgetItem(tripId, item.id) } + catch { toast.error(t('common.error')) } + } + const handleRenameCategory = async (oldName: string, newName: string) => { + if (!newName.trim() || newName.trim() === oldName) return + const items = grouped.get(oldName) || [] + try { for (const item of Array.from(items)) await updateBudgetItem(tripId, item.id, { category: newName.trim() }) } + catch { toast.error(t('common.error')) } + } + const handleAddCategory = () => { + if (!newCategoryName.trim()) return + Promise.resolve(addBudgetItem(tripId, { name: t('budget.defaultEntry'), category: newCategoryName.trim(), total_price: 0 })) + .catch(() => toast.error(t('common.error'))) + setNewCategoryName('') + } + + const handleExportCsv = () => { + const sep = ';' + const esc = (v: unknown) => { const s = String(v ?? ''); return s.includes(sep) || s.includes('"') || s.includes('\n') ? '"' + s.replace(/"/g, '""') + '"' : s } + const d = currencyDecimals(currency) + const fmtPrice = (v: number | null | undefined) => v != null ? v.toFixed(d) : '' + + const fmtDate = (iso: string) => { if (!iso) return ''; const d = new Date(iso + 'T00:00:00Z'); return d.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: 'UTC' }) } + const header = ['Category', 'Name', 'Date', 'Total (' + currency + ')', 'Persons', 'Days', 'Per Person', 'Per Day', 'Per Person/Day', 'Note'] + const rows = [header.join(sep)] + + for (const cat of categoryNames) { + for (const item of (grouped.get(cat) || [])) { + const pp = calcPP(item.total_price, item.persons) + const pd = calcPD(item.total_price, item.days) + const ppd = calcPPD(item.total_price, item.persons, item.days) + rows.push([ + esc(item.category), esc(item.name), esc(fmtDate(item.expense_date || '')), + fmtPrice(item.total_price), item.persons ?? '', item.days ?? '', + fmtPrice(pp), fmtPrice(pd), fmtPrice(ppd), + esc(item.note || ''), + ].join(sep)) + } + } + + const bom = '' + const blob = new Blob([bom + rows.join('\r\n')], { type: 'text/csv;charset=utf-8;' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + const safeName = (trip?.title || 'trip').replace(/[^a-zA-Z0-9À-ɏ _-]/g, '').trim() + a.download = `budget-${safeName}.csv` + a.click() + URL.revokeObjectURL(url) + } + + const th: CSSProperties = { padding: '6px 8px', textAlign: 'center', fontSize: 11, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', borderBottom: '2px solid var(--border-primary)', whiteSpace: 'nowrap', background: 'var(--bg-secondary)' } + const td: CSSProperties = { padding: '2px 6px', borderBottom: '1px solid var(--border-secondary)', fontSize: 13, verticalAlign: 'middle', color: 'var(--text-primary)' } + + return { + trip, budgetItems, + setBudgetItemMembers, toggleBudgetMemberPaid, reorderBudgetItems, reorderBudgetCategories, + t, locale, isDark, theme, + newCategoryName, setNewCategoryName, + editingCat, setEditingCat, + settlement, settlementOpen, setSettlementOpen, + currency, canEdit, fmt, hasMultipleMembers, + dragCat, setDragCat, dragOverCat, setDragOverCat, + dragItem, setDragItem, dragOverItem, setDragOverItem, dragItemCat, setDragItemCat, + setCurrency, + grouped, categoryNames, categoryColor, grandTotal, pieSegments, + handleAddItem, handleUpdateField, handleDeleteItem, handleDeleteCategory, handleRenameCategory, handleAddCategory, handleExportCsv, + th, td, + } +} diff --git a/client/src/components/Collab/CollabChat.constants.ts b/client/src/components/Collab/CollabChat.constants.ts new file mode 100644 index 00000000..3b8794cd --- /dev/null +++ b/client/src/components/Collab/CollabChat.constants.ts @@ -0,0 +1,10 @@ +export const EMOJI_CATEGORIES = { + 'Smileys': ['😀','😂','🥹','😍','🤩','😎','🥳','😭','🤔','👀','🙈','🫠','😴','🤯','🥺','😤','💀','👻','🫡','🤝'], + 'Reactions': ['❤️','🔥','👍','👎','👏','🎉','💯','✨','⭐','💪','🙏','😱','😂','💖','💕','🤞','✅','❌','⚡','🏆'], + 'Travel': ['✈️','🏖️','🗺️','🧳','🏔️','🌅','🌴','🚗','🚂','🛳️','🏨','🍽️','🍕','🍹','📸','🎒','⛱️','🌍','🗼','🎌'], +} + +// Reaction Quick Menu (right-click) +export const QUICK_REACTIONS = ['❤️', '😂', '👍', '😮', '😢', '🔥', '👏', '🎉'] + +export const URL_REGEX = /https?:\/\/[^\s<>"']+/g diff --git a/client/src/components/Collab/CollabChat.helpers.ts b/client/src/components/Collab/CollabChat.helpers.ts new file mode 100644 index 00000000..84de232c --- /dev/null +++ b/client/src/components/Collab/CollabChat.helpers.ts @@ -0,0 +1,42 @@ +// ── Twemoji helper (Apple-style emojis via CDN) ── +export function emojiToCodepoint(emoji) { + const codepoints = [] + for (const c of emoji) { + const cp = c.codePointAt(0) + if (cp !== 0xfe0f) codepoints.push(cp.toString(16)) // skip variation selector + } + return codepoints.join('-') +} + +// SQLite stores UTC without 'Z' suffix — append it so JS parses as UTC +export function parseUTC(s) { return new Date(s && !s.endsWith('Z') ? s + 'Z' : s) } + +export function formatTime(isoString, is12h) { + const d = parseUTC(isoString) + const h = d.getHours() + const mm = String(d.getMinutes()).padStart(2, '0') + if (is12h) { + const period = h >= 12 ? 'PM' : 'AM' + const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h + return `${h12}:${mm} ${period}` + } + return `${String(h).padStart(2, '0')}:${mm}` +} + +export function formatDateSeparator(isoString, t) { + const d = parseUTC(isoString) + const now = new Date() + const yesterday = new Date(); yesterday.setDate(now.getDate() - 1) + + if (d.toDateString() === now.toDateString()) return t('collab.chat.today') || 'Today' + if (d.toDateString() === yesterday.toDateString()) return t('collab.chat.yesterday') || 'Yesterday' + + return d.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }) +} + +export function shouldShowDateSeparator(msg, prevMsg) { + if (!prevMsg) return true + const d1 = parseUTC(msg.created_at).toDateString() + const d2 = parseUTC(prevMsg.created_at).toDateString() + return d1 !== d2 +} diff --git a/client/src/components/Collab/CollabChat.tsx b/client/src/components/Collab/CollabChat.tsx index 2735029b..94b7f1b0 100644 --- a/client/src/components/Collab/CollabChat.tsx +++ b/client/src/components/Collab/CollabChat.tsx @@ -1,350 +1,10 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react' import ReactDOM from 'react-dom' -import { ArrowUp, Trash2, Reply, ChevronUp, MessageCircle, Smile, X } from 'lucide-react' -import { collabApi } from '../../api/client' -import { useSettingsStore } from '../../store/settingsStore' -import { useCanDo } from '../../store/permissionsStore' -import { useTripStore } from '../../store/tripStore' -import { addListener, removeListener } from '../../api/websocket' -import { useTranslation } from '../../i18n' +import { ArrowUp, Reply, Smile, X } from 'lucide-react' import type { User } from '../../types' - -interface ChatReaction { - emoji: string - count: number - users: { id: number; username: string }[] -} - -interface ChatMessage { - id: number - trip_id: number - user_id: number - text: string - reply_to_id: number | null - reactions: ChatReaction[] - created_at: string - user?: { username: string; avatar_url: string | null } - reply_to?: ChatMessage | null -} - -// ── Twemoji helper (Apple-style emojis via CDN) ── -function emojiToCodepoint(emoji) { - const codepoints = [] - for (const c of emoji) { - const cp = c.codePointAt(0) - if (cp !== 0xfe0f) codepoints.push(cp.toString(16)) // skip variation selector - } - return codepoints.join('-') -} - -function TwemojiImg({ emoji, size = 20, style = {} }) { - const cp = emojiToCodepoint(emoji) - const [failed, setFailed] = useState(false) - - if (failed) { - return {emoji} - } - - return ( - {emoji} setFailed(true)} - /> - ) -} - -const EMOJI_CATEGORIES = { - 'Smileys': ['😀','😂','🥹','😍','🤩','😎','🥳','😭','🤔','👀','🙈','🫠','😴','🤯','🥺','😤','💀','👻','🫡','🤝'], - 'Reactions': ['❤️','🔥','👍','👎','👏','🎉','💯','✨','⭐','💪','🙏','😱','😂','💖','💕','🤞','✅','❌','⚡','🏆'], - 'Travel': ['✈️','🏖️','🗺️','🧳','🏔️','🌅','🌴','🚗','🚂','🛳️','🏨','🍽️','🍕','🍹','📸','🎒','⛱️','🌍','🗼','🎌'], -} - -// SQLite stores UTC without 'Z' suffix — append it so JS parses as UTC -function parseUTC(s) { return new Date(s && !s.endsWith('Z') ? s + 'Z' : s) } - -function formatTime(isoString, is12h) { - const d = parseUTC(isoString) - const h = d.getHours() - const mm = String(d.getMinutes()).padStart(2, '0') - if (is12h) { - const period = h >= 12 ? 'PM' : 'AM' - const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h - return `${h12}:${mm} ${period}` - } - return `${String(h).padStart(2, '0')}:${mm}` -} - -function formatDateSeparator(isoString, t) { - const d = parseUTC(isoString) - const now = new Date() - const yesterday = new Date(); yesterday.setDate(now.getDate() - 1) - - if (d.toDateString() === now.toDateString()) return t('collab.chat.today') || 'Today' - if (d.toDateString() === yesterday.toDateString()) return t('collab.chat.yesterday') || 'Yesterday' - - return d.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }) -} - -function shouldShowDateSeparator(msg, prevMsg) { - if (!prevMsg) return true - const d1 = parseUTC(msg.created_at).toDateString() - const d2 = parseUTC(prevMsg.created_at).toDateString() - return d1 !== d2 -} - -/* ── Emoji Picker ── */ -interface EmojiPickerProps { - onSelect: (emoji: string) => void - onClose: () => void - anchorRef: React.RefObject - containerRef: React.RefObject -} - -function EmojiPicker({ onSelect, onClose, anchorRef, containerRef }: EmojiPickerProps) { - const [cat, setCat] = useState(Object.keys(EMOJI_CATEGORIES)[0]) - const ref = useRef(null) - - const getPos = () => { - const container = containerRef?.current - const anchor = anchorRef?.current - if (container && anchor) { - const cRect = container.getBoundingClientRect() - const aRect = anchor.getBoundingClientRect() - return { bottom: window.innerHeight - aRect.top + 16, left: cRect.left + cRect.width / 2 - 140 } - } - return { bottom: 80, left: 0 } - } - const pos = getPos() - - useEffect(() => { - const close = (e) => { - if (ref.current && ref.current.contains(e.target)) return - if (anchorRef?.current && anchorRef.current.contains(e.target)) return - onClose() - } - document.addEventListener('mousedown', close) - return () => document.removeEventListener('mousedown', close) - }, [onClose, anchorRef]) - - return ReactDOM.createPortal( -
- {/* Category tabs */} -
- {Object.keys(EMOJI_CATEGORIES).map(c => ( - - ))} -
- {/* Emoji grid */} -
- {EMOJI_CATEGORIES[cat].map((emoji, i) => ( - - ))} -
-
, - document.body - ) -} - -/* ── Reaction Quick Menu (right-click) ── */ -const QUICK_REACTIONS = ['❤️', '😂', '👍', '😮', '😢', '🔥', '👏', '🎉'] - -interface ReactionMenuProps { - x: number - y: number - onReact: (emoji: string) => void - onClose: () => void -} - -function ReactionMenu({ x, y, onReact, onClose }: ReactionMenuProps) { - const ref = useRef(null) - - useEffect(() => { - const close = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose() } - document.addEventListener('mousedown', close) - return () => document.removeEventListener('mousedown', close) - }, [onClose]) - - // Clamp to viewport - const menuWidth = 156 - const clampedLeft = Math.max(menuWidth / 2 + 8, Math.min(x, window.innerWidth - menuWidth / 2 - 8)) - - return ( -
- {QUICK_REACTIONS.map(emoji => ( - - ))} -
- ) -} - -/* ── Message Text with clickable URLs ── */ -interface MessageTextProps { - text: string -} - -function MessageText({ text }: MessageTextProps) { - const parts = text.split(URL_REGEX) - const urls = text.match(URL_REGEX) || [] - const result = [] - parts.forEach((part, i) => { - if (part) result.push(part) - if (urls[i]) result.push( -
- {urls[i]} - - ) - }) - return <>{result} -} - -/* ── Link Preview ── */ -const URL_REGEX = /https?:\/\/[^\s<>"']+/g -const previewCache = {} - -interface LinkPreviewProps { - url: string - tripId: number - own: boolean - onLoad: (() => void) | undefined -} - -function LinkPreview({ url, tripId, own, onLoad }: LinkPreviewProps) { - const [data, setData] = useState(previewCache[url] || null) - const [loading, setLoading] = useState(!previewCache[url]) - - useEffect(() => { - if (previewCache[url]) return - collabApi.linkPreview(tripId, url).then(d => { - previewCache[url] = d - setData(d) - setLoading(false) - if (d?.title || d?.description || d?.image) onLoad?.() - }).catch(() => setLoading(false)) - }, [url, tripId]) - - if (loading || !data || (!data.title && !data.description && !data.image)) return null - - const domain = (() => { try { return new URL(url).hostname.replace('www.', '') } catch { return '' } })() - - return ( - e.currentTarget.style.opacity = '0.85'} - onMouseLeave={e => e.currentTarget.style.opacity = '1'} - > - {data.image && ( - e.target.style.display = 'none'} /> - )} -
- {domain && ( -
- {data.site_name || domain} -
- )} - {data.title && ( -
- {data.title} -
- )} - {data.description && ( -
- {data.description} -
- )} -
-
- ) -} - -/* ── Reaction Badge with NOMAD tooltip ── */ -interface ReactionBadgeProps { - reaction: ChatReaction - currentUserId: number - onReact: () => void -} - -function ReactionBadge({ reaction, currentUserId, onReact }: ReactionBadgeProps) { - const [hover, setHover] = useState(false) - const [pos, setPos] = useState({ top: 0, left: 0 }) - const ref = useRef(null) - const names = reaction.users.map(u => u.username).join(', ') - - return ( - <> - - {hover && names && ReactDOM.createPortal( -
- {names} -
, - document.body - )} - - ) -} +import { useCollabChat } from './useCollabChat' +import { ChatMessages } from './CollabChatMessages' +import { EmojiPicker } from './CollabChatEmojiPicker' +import { ReactionMenu } from './CollabChatReactionMenu' /* ── Main Component ── */ interface CollabChatProps { @@ -353,173 +13,8 @@ interface CollabChatProps { } export default function CollabChat({ tripId, currentUser }: CollabChatProps) { - const { t } = useTranslation() - const is12h = useSettingsStore(s => s.settings.time_format) === '12h' - const can = useCanDo() - const trip = useTripStore((s) => s.trip) - const canEdit = can('collab_edit', trip) - - const [messages, setMessages] = useState([]) - const [loading, setLoading] = useState(true) - const [hasMore, setHasMore] = useState(false) - const [loadingMore, setLoadingMore] = useState(false) - const [text, setText] = useState('') - const [replyTo, setReplyTo] = useState(null) - const [hoveredId, setHoveredId] = useState(null) - const [sending, setSending] = useState(false) - const [showEmoji, setShowEmoji] = useState(false) - const [reactMenu, setReactMenu] = useState(null) // { msgId, x, y } - const [deletingIds, setDeletingIds] = useState(new Set()) - const deleteTimersRef = useRef[]>([]) - - useEffect(() => { - return () => { deleteTimersRef.current.forEach(clearTimeout) } - }, []) - - const containerRef = useRef(null) - const messagesRef = useRef(messages) - messagesRef.current = messages - const scrollRef = useRef(null) - const textareaRef = useRef(null) - const emojiBtnRef = useRef(null) - const isAtBottom = useRef(true) - - const scrollToBottom = useCallback((behavior = 'auto') => { - const el = scrollRef.current - if (!el) return - requestAnimationFrame(() => el.scrollTo({ top: el.scrollHeight, behavior })) - }, []) - - const checkAtBottom = useCallback(() => { - const el = scrollRef.current - if (!el) return - isAtBottom.current = el.scrollHeight - el.scrollTop - el.clientHeight < 48 - }, []) - - /* ── load messages ── */ - useEffect(() => { - let cancelled = false - setLoading(true) - collabApi.getMessages(tripId).then(data => { - if (cancelled) return - const msgs = (Array.isArray(data) ? data : data.messages || []).map(m => m.deleted ? { ...m, _deleted: true } : m) - setMessages(msgs) - setHasMore(msgs.length >= 100) - setLoading(false) - setTimeout(() => scrollToBottom(), 30) - }).catch(() => { if (!cancelled) setLoading(false) }) - return () => { cancelled = true } - }, [tripId, scrollToBottom]) - - /* ── load more ── */ - const handleLoadMore = useCallback(async () => { - if (loadingMore || messages.length === 0) return - setLoadingMore(true) - const el = scrollRef.current - const prevHeight = el ? el.scrollHeight : 0 - try { - const data = await collabApi.getMessages(tripId, messages[0]?.id) - const older = (Array.isArray(data) ? data : data.messages || []).map(m => m.deleted ? { ...m, _deleted: true } : m) - if (older.length === 0) { setHasMore(false) } - else { - setMessages(prev => [...older, ...prev]) - setHasMore(older.length >= 100) - requestAnimationFrame(() => { if (el) el.scrollTop = el.scrollHeight - prevHeight }) - } - } catch {} finally { setLoadingMore(false) } - }, [tripId, loadingMore, messages]) - - /* ── websocket ── */ - useEffect(() => { - const handler = (event) => { - if (event.type === 'collab:message:created' && String(event.tripId) === String(tripId)) { - setMessages(prev => prev.some(m => m.id === event.message.id) ? prev : [...prev, event.message]) - if (isAtBottom.current) setTimeout(() => scrollToBottom('smooth'), 30) - } - if (event.type === 'collab:message:deleted' && String(event.tripId) === String(tripId)) { - setMessages(prev => prev.map(m => m.id === event.messageId ? { ...m, _deleted: true } : m)) - if (isAtBottom.current) setTimeout(() => scrollToBottom('smooth'), 50) - } - if (event.type === 'collab:message:reacted' && String(event.tripId) === String(tripId)) { - setMessages(prev => prev.map(m => m.id === event.messageId ? { ...m, reactions: event.reactions } : m)) - } - } - addListener(handler) - return () => removeListener(handler) - }, [tripId, scrollToBottom]) - - /* ── auto-resize textarea ── */ - const handleTextChange = useCallback((e) => { - setText(e.target.value) - const ta = textareaRef.current - if (ta) { - ta.style.height = 'auto' - const h = Math.min(ta.scrollHeight, 100) - ta.style.height = h + 'px' - ta.style.overflowY = ta.scrollHeight > 100 ? 'auto' : 'hidden' - } - }, []) - - /* ── send ── */ - const handleSend = useCallback(async () => { - const body = text.trim() - if (!body || sending) return - setSending(true) - try { - const payload = { text: body } - if (replyTo) payload.reply_to = replyTo.id - const data = await collabApi.sendMessage(tripId, payload) - if (data?.message) { - setMessages(prev => prev.some(m => m.id === data.message.id) ? prev : [...prev, data.message]) - } - setText(''); setReplyTo(null); setShowEmoji(false) - if (textareaRef.current) textareaRef.current.style.height = 'auto' - isAtBottom.current = true - setTimeout(() => scrollToBottom('smooth'), 50) - } catch {} finally { setSending(false) } - }, [text, sending, replyTo, tripId, scrollToBottom]) - - const handleKeyDown = useCallback((e) => { - if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend() } - }, [handleSend]) - - const handleDelete = useCallback(async (msgId) => { - const msg = messages.find(m => m.id === msgId) - requestAnimationFrame(() => { - setDeletingIds(prev => new Set(prev).add(msgId)) - }) - const t = setTimeout(async () => { - try { - await collabApi.deleteMessage(tripId, msgId) - setMessages(prev => prev.map(m => m.id === msgId ? { ...m, _deleted: true } : m)) - } catch {} - setDeletingIds(prev => { const s = new Set(prev); s.delete(msgId); return s }) - }, 400) - deleteTimersRef.current.push(t) - }, [tripId]) - - const handleReact = useCallback(async (msgId, emoji) => { - setReactMenu(null) - try { - const data = await collabApi.reactMessage(tripId, msgId, emoji) - setMessages(prev => prev.map(m => m.id === msgId ? { ...m, reactions: data.reactions } : m)) - } catch {} - }, [tripId]) - - const handleEmojiSelect = useCallback((emoji) => { - setText(prev => prev + emoji) - textareaRef.current?.focus() - }, []) - - const isOwn = (msg) => String(msg.user_id) === String(currentUser.id) - - // Check if message is only emoji (1-3 emojis, no other text) - const isEmojiOnly = (text) => { - const emojiRegex = /^(?:\p{Emoji_Presentation}|\p{Extended_Pictographic}[\uFE0F]?(?:\u200D\p{Extended_Pictographic}[\uFE0F]?)*){1,3}$/u - return emojiRegex.test(text.trim()) - } - - /* ── Loading ── */ + const S = useCollabChat(tripId, currentUser) + const { t, is12h, can, trip, canEdit, messages, setMessages, loading, setLoading, hasMore, setHasMore, loadingMore, setLoadingMore, text, setText, replyTo, setReplyTo, hoveredId, setHoveredId, sending, setSending, showEmoji, setShowEmoji, reactMenu, setReactMenu, deletingIds, setDeletingIds, deleteTimersRef, containerRef, messagesRef, scrollRef, textareaRef, emojiBtnRef, isAtBottom, scrollToBottom, checkAtBottom, handleLoadMore, handleTextChange, handleSend, handleKeyDown, handleDelete, handleReact, handleEmojiSelect, isOwn, isEmojiOnly } = S if (loading) { return (
@@ -528,247 +23,11 @@ export default function CollabChat({ tripId, currentUser }: CollabChatProps) {
) } - - /* ── Main ── */ return (
- {/* Messages */} - {messages.length === 0 ? ( -
- - {t('collab.chat.empty')} - {t('collab.chat.emptyDesc') || ''} -
- ) : ( -
- {hasMore && ( -
- -
- )} - - {messages.map((msg, idx) => { - const own = isOwn(msg) - const prevMsg = messages[idx - 1] - const nextMsg = messages[idx + 1] - const isNewGroup = idx === 0 || String(prevMsg?.user_id) !== String(msg.user_id) - const isLastInGroup = !nextMsg || String(nextMsg?.user_id) !== String(msg.user_id) - const showDate = shouldShowDateSeparator(msg, prevMsg) - const showAvatar = !own && isLastInGroup - const bigEmoji = isEmojiOnly(msg.text) - const hasReply = msg.reply_text || msg.reply_to - // Deleted message placeholder - if (msg._deleted) { - return ( - - {showDate && ( -
- - {formatDateSeparator(msg.created_at, t)} - -
- )} -
- - {msg.username} {t('collab.chat.deletedMessage') || 'deleted a message'} · {formatTime(msg.created_at, is12h)} - -
-
- ) - } - - // Bubble border radius — iMessage style tails - const br = own - ? `18px 18px ${isLastInGroup ? '4px' : '18px'} 18px` - : `18px 18px 18px ${isLastInGroup ? '4px' : '18px'}` - - return ( - - {/* Date separator */} - {showDate && ( -
- - {formatDateSeparator(msg.created_at, t)} - -
- )} - -
- {/* Avatar slot for others */} - {!own && ( -
- {showAvatar && ( - msg.user_avatar ? ( - - ) : ( -
- {(msg.username || '?')[0].toUpperCase()} -
- ) - )} -
- )} - -
- {/* Username for others at group start */} - {!own && isNewGroup && ( - - {msg.username} - - )} - - {/* Bubble */} -
setHoveredId(msg.id)} - onMouseLeave={() => setHoveredId(null)} - onContextMenu={e => { e.preventDefault(); if (canEdit) setReactMenu({ msgId: msg.id, x: e.clientX, y: e.clientY }) }} - onTouchEnd={e => { - const now = Date.now() - const lastTap = e.currentTarget.dataset.lastTap || 0 - if (now - lastTap < 300 && canEdit) { - e.preventDefault() - const touch = e.changedTouches?.[0] - if (touch) setReactMenu({ msgId: msg.id, x: touch.clientX, y: touch.clientY }) - } - e.currentTarget.dataset.lastTap = now - }} - > - {bigEmoji ? ( -
- {msg.text} -
- ) : ( -
- {/* Inline reply quote */} - {hasReply && ( -
-
- {msg.reply_username || ''} -
-
- {(msg.reply_text || '').slice(0, 80)} -
-
- )} - {hasReply ? ( -
- ) : } - {(msg.text.match(URL_REGEX) || []).slice(0, 1).map(url => ( - { if (isAtBottom.current) setTimeout(() => scrollToBottom('smooth'), 50) }} /> - ))} -
- )} - - {/* Hover actions */} -
- - {own && canEdit && ( - - )} -
-
- - {/* Reactions — iMessage style floating badge */} - {msg.reactions?.length > 0 && ( -
-
- {msg.reactions.map(r => { - const myReaction = r.users.some(u => String(u.user_id) === String(currentUser.id)) - return ( - { if (canEdit) handleReact(msg.id, r.emoji) }} /> - ) - })} -
-
- )} - - {/* Timestamp — only on last message of group */} - {isLastInGroup && ( - - {formatTime(msg.created_at, is12h)} - - )} -
-
-
- ) - })} -
- )} - + {/* Composer */} -
+
{/* Reply preview */} {replyTo && (
void + onClose: () => void + anchorRef: React.RefObject + containerRef: React.RefObject +} + +export function EmojiPicker({ onSelect, onClose, anchorRef, containerRef }: EmojiPickerProps) { + const [cat, setCat] = useState(Object.keys(EMOJI_CATEGORIES)[0]) + const ref = useRef(null) + + const getPos = () => { + const container = containerRef?.current + const anchor = anchorRef?.current + if (container && anchor) { + const cRect = container.getBoundingClientRect() + const aRect = anchor.getBoundingClientRect() + return { bottom: window.innerHeight - aRect.top + 16, left: cRect.left + cRect.width / 2 - 140 } + } + return { bottom: 80, left: 0 } + } + const pos = getPos() + + useEffect(() => { + const close = (e) => { + if (ref.current && ref.current.contains(e.target)) return + if (anchorRef?.current && anchorRef.current.contains(e.target)) return + onClose() + } + document.addEventListener('mousedown', close) + return () => document.removeEventListener('mousedown', close) + }, [onClose, anchorRef]) + + return ReactDOM.createPortal( +
+ {/* Category tabs */} +
+ {Object.keys(EMOJI_CATEGORIES).map(c => ( + + ))} +
+ {/* Emoji grid */} +
+ {EMOJI_CATEGORIES[cat].map((emoji, i) => ( + + ))} +
+
, + document.body + ) +} diff --git a/client/src/components/Collab/CollabChatLinkPreview.tsx b/client/src/components/Collab/CollabChatLinkPreview.tsx new file mode 100644 index 00000000..70d282b2 --- /dev/null +++ b/client/src/components/Collab/CollabChatLinkPreview.tsx @@ -0,0 +1,65 @@ +import { useState, useEffect } from 'react' +import { collabApi } from '../../api/client' + +/* ── Link Preview ── */ +const previewCache = {} + +interface LinkPreviewProps { + url: string + tripId: number + own: boolean + onLoad: (() => void) | undefined +} + +export function LinkPreview({ url, tripId, own, onLoad }: LinkPreviewProps) { + const [data, setData] = useState(previewCache[url] || null) + const [loading, setLoading] = useState(!previewCache[url]) + + useEffect(() => { + if (previewCache[url]) return + collabApi.linkPreview(tripId, url).then(d => { + previewCache[url] = d + setData(d) + setLoading(false) + if (d?.title || d?.description || d?.image) onLoad?.() + }).catch(() => setLoading(false)) + }, [url, tripId]) + + if (loading || !data || (!data.title && !data.description && !data.image)) return null + + const domain = (() => { try { return new URL(url).hostname.replace('www.', '') } catch { return '' } })() + + return ( + e.currentTarget.style.opacity = '0.85'} + onMouseLeave={e => e.currentTarget.style.opacity = '1'} + > + {data.image && ( + e.currentTarget.style.display = 'none'} /> + )} +
+ {domain && ( +
+ {data.site_name || domain} +
+ )} + {data.title && ( +
+ {data.title} +
+ )} + {data.description && ( +
+ {data.description} +
+ )} +
+
+ ) +} diff --git a/client/src/components/Collab/CollabChatMessageText.tsx b/client/src/components/Collab/CollabChatMessageText.tsx new file mode 100644 index 00000000..795cf735 --- /dev/null +++ b/client/src/components/Collab/CollabChatMessageText.tsx @@ -0,0 +1,21 @@ +import { URL_REGEX } from './CollabChat.constants' + +/* ── Message Text with clickable URLs ── */ +interface MessageTextProps { + text: string +} + +export function MessageText({ text }: MessageTextProps) { + const parts = text.split(URL_REGEX) + const urls = text.match(URL_REGEX) || [] + const result = [] + parts.forEach((part, i) => { + if (part) result.push(part) + if (urls[i]) result.push( + + {urls[i]} + + ) + }) + return <>{result} +} diff --git a/client/src/components/Collab/CollabChatMessages.tsx b/client/src/components/Collab/CollabChatMessages.tsx new file mode 100644 index 00000000..37a4661d --- /dev/null +++ b/client/src/components/Collab/CollabChatMessages.tsx @@ -0,0 +1,250 @@ +import React from 'react' +import { Trash2, Reply, ChevronUp, MessageCircle } from 'lucide-react' +import { URL_REGEX } from './CollabChat.constants' +import { formatTime, formatDateSeparator, shouldShowDateSeparator } from './CollabChat.helpers' +import { MessageText } from './CollabChatMessageText' +import { LinkPreview } from './CollabChatLinkPreview' +import { ReactionBadge } from './CollabChatReactionBadge' + +export function ChatMessages(props: any) { + const { currentUser, tripId, t, is12h, can, trip, canEdit, messages, setMessages, loading, setLoading, hasMore, setHasMore, loadingMore, setLoadingMore, text, setText, replyTo, setReplyTo, hoveredId, setHoveredId, sending, setSending, showEmoji, setShowEmoji, reactMenu, setReactMenu, deletingIds, setDeletingIds, deleteTimersRef, containerRef, messagesRef, scrollRef, textareaRef, emojiBtnRef, isAtBottom, scrollToBottom, checkAtBottom, handleLoadMore, handleTextChange, handleSend, handleKeyDown, handleDelete, handleReact, handleEmojiSelect, isOwn, isEmojiOnly } = props + return ( + <> + {/* Messages */} + {messages.length === 0 ? ( +
+ + {t('collab.chat.empty')} + {t('collab.chat.emptyDesc') || ''} +
+ ) : ( +
+ {hasMore && ( +
+ +
+ )} + + {messages.map((msg, idx) => { + const own = isOwn(msg) + const prevMsg = messages[idx - 1] + const nextMsg = messages[idx + 1] + const isNewGroup = idx === 0 || String(prevMsg?.user_id) !== String(msg.user_id) + const isLastInGroup = !nextMsg || String(nextMsg?.user_id) !== String(msg.user_id) + const showDate = shouldShowDateSeparator(msg, prevMsg) + const showAvatar = !own && isLastInGroup + const bigEmoji = isEmojiOnly(msg.text) + const hasReply = msg.reply_text || msg.reply_to + // Deleted message placeholder + if (msg._deleted) { + return ( + + {showDate && ( +
+ + {formatDateSeparator(msg.created_at, t)} + +
+ )} +
+ + {msg.username} {t('collab.chat.deletedMessage') || 'deleted a message'} · {formatTime(msg.created_at, is12h)} + +
+
+ ) + } + + // Bubble border radius — iMessage style tails + const br = own + ? `18px 18px ${isLastInGroup ? '4px' : '18px'} 18px` + : `18px 18px 18px ${isLastInGroup ? '4px' : '18px'}` + + return ( + + {/* Date separator */} + {showDate && ( +
+ + {formatDateSeparator(msg.created_at, t)} + +
+ )} + +
+ {/* Avatar slot for others */} + {!own && ( +
+ {showAvatar && ( + msg.user_avatar ? ( + + ) : ( +
+ {(msg.username || '?')[0].toUpperCase()} +
+ ) + )} +
+ )} + +
+ {/* Username for others at group start */} + {!own && isNewGroup && ( + + {msg.username} + + )} + + {/* Bubble */} +
setHoveredId(msg.id)} + onMouseLeave={() => setHoveredId(null)} + onContextMenu={e => { e.preventDefault(); if (canEdit) setReactMenu({ msgId: msg.id, x: e.clientX, y: e.clientY }) }} + onTouchEnd={e => { + const now = Date.now() + const lastTap = Number(e.currentTarget.dataset.lastTap) || 0 + if (now - lastTap < 300 && canEdit) { + e.preventDefault() + const touch = e.changedTouches?.[0] + if (touch) setReactMenu({ msgId: msg.id, x: touch.clientX, y: touch.clientY }) + } + e.currentTarget.dataset.lastTap = String(now) + }} + > + {bigEmoji ? ( +
+ {msg.text} +
+ ) : ( +
+ {/* Inline reply quote */} + {hasReply && ( +
+
+ {msg.reply_username || ''} +
+
+ {(msg.reply_text || '').slice(0, 80)} +
+
+ )} + {hasReply ? ( +
+ ) : } + {(msg.text.match(URL_REGEX) || []).slice(0, 1).map(url => ( + { if (isAtBottom.current) setTimeout(() => scrollToBottom('smooth'), 50) }} /> + ))} +
+ )} + + {/* Hover actions */} +
+ + {own && canEdit && ( + + )} +
+
+ + {/* Reactions — iMessage style floating badge */} + {msg.reactions?.length > 0 && ( +
+
+ {msg.reactions.map(r => { + const myReaction = r.users.some(u => String(u.user_id) === String(currentUser.id)) + return ( + { if (canEdit) handleReact(msg.id, r.emoji) }} /> + ) + })} +
+
+ )} + + {/* Timestamp — only on last message of group */} + {isLastInGroup && ( + + {formatTime(msg.created_at, is12h)} + + )} +
+
+
+ ) + })} +
+ )} + + + ) +} diff --git a/client/src/components/Collab/CollabChatReactionBadge.tsx b/client/src/components/Collab/CollabChatReactionBadge.tsx new file mode 100644 index 00000000..43122137 --- /dev/null +++ b/client/src/components/Collab/CollabChatReactionBadge.tsx @@ -0,0 +1,53 @@ +import { useState, useRef } from 'react' +import ReactDOM from 'react-dom' +import { TwemojiImg } from './CollabChatTwemojiImg' +import type { ChatReaction } from './CollabChat.types' + +/* ── Reaction Badge with NOMAD tooltip ── */ +interface ReactionBadgeProps { + reaction: ChatReaction + currentUserId: number + onReact: () => void +} + +export function ReactionBadge({ reaction, currentUserId, onReact }: ReactionBadgeProps) { + const [hover, setHover] = useState(false) + const [pos, setPos] = useState({ top: 0, left: 0 }) + const ref = useRef(null) + const names = reaction.users.map(u => u.username).join(', ') + + return ( + <> + + {hover && names && ReactDOM.createPortal( +
+ {names} +
, + document.body + )} + + ) +} diff --git a/client/src/components/Collab/CollabChatReactionMenu.tsx b/client/src/components/Collab/CollabChatReactionMenu.tsx new file mode 100644 index 00000000..57e74a1e --- /dev/null +++ b/client/src/components/Collab/CollabChatReactionMenu.tsx @@ -0,0 +1,47 @@ +import { useEffect, useRef } from 'react' +import { QUICK_REACTIONS } from './CollabChat.constants' +import { TwemojiImg } from './CollabChatTwemojiImg' + +/* ── Reaction Quick Menu (right-click) ── */ +interface ReactionMenuProps { + x: number + y: number + onReact: (emoji: string) => void + onClose: () => void +} + +export function ReactionMenu({ x, y, onReact, onClose }: ReactionMenuProps) { + const ref = useRef(null) + + useEffect(() => { + const close = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose() } + document.addEventListener('mousedown', close) + return () => document.removeEventListener('mousedown', close) + }, [onClose]) + + // Clamp to viewport + const menuWidth = 156 + const clampedLeft = Math.max(menuWidth / 2 + 8, Math.min(x, window.innerWidth - menuWidth / 2 - 8)) + + return ( +
+ {QUICK_REACTIONS.map(emoji => ( + + ))} +
+ ) +} diff --git a/client/src/components/Collab/CollabChatTwemojiImg.tsx b/client/src/components/Collab/CollabChatTwemojiImg.tsx new file mode 100644 index 00000000..6e29538e --- /dev/null +++ b/client/src/components/Collab/CollabChatTwemojiImg.tsx @@ -0,0 +1,21 @@ +import { useState } from 'react' +import { emojiToCodepoint } from './CollabChat.helpers' + +export function TwemojiImg({ emoji, size = 20, style = {} }) { + const cp = emojiToCodepoint(emoji) + const [failed, setFailed] = useState(false) + + if (failed) { + return {emoji} + } + + return ( + {emoji} setFailed(true)} + /> + ) +} diff --git a/client/src/components/Collab/CollabNotes.constants.ts b/client/src/components/Collab/CollabNotes.constants.ts new file mode 100644 index 00000000..855bc28d --- /dev/null +++ b/client/src/components/Collab/CollabNotes.constants.ts @@ -0,0 +1,10 @@ +export const FONT = "var(--font-system)" + +export const NOTE_COLORS = [ + { value: '#6366f1', label: 'Indigo' }, + { value: '#ef4444', label: 'Red' }, + { value: '#f59e0b', label: 'Amber' }, + { value: '#10b981', label: 'Emerald' }, + { value: '#3b82f6', label: 'Blue' }, + { value: '#8b5cf6', label: 'Violet' }, +] diff --git a/client/src/components/Collab/CollabNotes.helpers.ts b/client/src/components/Collab/CollabNotes.helpers.ts new file mode 100644 index 00000000..add4605c --- /dev/null +++ b/client/src/components/Collab/CollabNotes.helpers.ts @@ -0,0 +1,16 @@ +// Pure formatting helper for note timestamps. Falls back to translated +// relative labels for recent timestamps and a localized short date beyond a week. +export const formatTimestamp = (ts, t, locale) => { + if (!ts) return '' + const d = new Date(ts.endsWith?.('Z') ? ts : ts + 'Z') + const now = new Date() + const diffMs = now.getTime() - d.getTime() + const diffMins = Math.floor(diffMs / 60000) + if (diffMins < 1) return t('collab.chat.justNow') || 'just now' + if (diffMins < 60) return t('collab.chat.minutesAgo', { n: diffMins }) || `${diffMins}m ago` + const diffHrs = Math.floor(diffMins / 60) + if (diffHrs < 24) return t('collab.chat.hoursAgo', { n: diffHrs }) || `${diffHrs}h ago` + const diffDays = Math.floor(diffHrs / 24) + if (diffDays < 7) return t('collab.notes.daysAgo', { n: diffDays }) || `${diffDays}d ago` + return d.toLocaleDateString(locale || undefined, { month: 'short', day: 'numeric' }) +} diff --git a/client/src/components/Collab/CollabNotes.test.tsx b/client/src/components/Collab/CollabNotes.test.tsx index 9c8fc884..73aa6c3b 100644 --- a/client/src/components/Collab/CollabNotes.test.tsx +++ b/client/src/components/Collab/CollabNotes.test.tsx @@ -175,7 +175,7 @@ describe('CollabNotes', () => { expect(document.body).toBeInTheDocument(); }); - it('FE-COMP-NOTES-013: delete note calls DELETE API and removes it from grid', async () => { + it('FE-COMP-NOTES-013: deleting a note asks for confirmation, then calls DELETE API and removes it', async () => { const user = userEvent.setup(); server.use( http.get('/api/trips/1/collab/notes', () => @@ -193,8 +193,11 @@ describe('CollabNotes', () => { ); render(); await screen.findByText('Remove Me'); - const deleteBtn = screen.getByTitle('Delete'); - await user.click(deleteBtn); + await user.click(screen.getByTitle('Delete')); + // Deleting now asks for confirmation first — the note stays until confirmed. + expect(screen.getByText('Delete note?')).toBeInTheDocument(); + expect(screen.getByText('Remove Me')).toBeInTheDocument(); + await user.click(document.querySelector('button.bg-red-600') as HTMLElement); await waitFor(() => expect(screen.queryByText('Remove Me')).not.toBeInTheDocument()); }); diff --git a/client/src/components/Collab/CollabNotes.tsx b/client/src/components/Collab/CollabNotes.tsx index 2d6f253c..1a1556f8 100644 --- a/client/src/components/Collab/CollabNotes.tsx +++ b/client/src/components/Collab/CollabNotes.tsx @@ -1,904 +1,24 @@ -import ReactDOM from 'react-dom' -import { useState, useEffect, useCallback, useRef, useMemo } from 'react' -import DOM from 'react-dom' +import { useState, useEffect, useCallback, useMemo } from 'react' import Markdown from 'react-markdown' import remarkGfm from 'remark-gfm' import remarkBreaks from 'remark-breaks' -import { Plus, Trash2, Pin, PinOff, Pencil, X, Check, StickyNote, Settings, ExternalLink, Maximize2, Loader2 } from 'lucide-react' +import ReactDOM from 'react-dom' +import { Plus, Pencil, X, StickyNote, Settings } from 'lucide-react' import { collabApi } from '../../api/client' -import { getAuthUrl } from '../../api/authUrl' -import { openFile } from '../../utils/fileDownload' import { useCanDo } from '../../store/permissionsStore' import { useTripStore } from '../../store/tripStore' import { addListener, removeListener } from '../../api/websocket' import { useTranslation } from '../../i18n' +import { useToast } from '../shared/Toast' +import ConfirmDialog from '../shared/ConfirmDialog' import type { User } from '../../types' - -interface NoteFile { - id: number - filename: string - original_name: string - mime_type: string - url?: string -} - -interface CollabNote { - id: number - trip_id: number - title: string - content: string - category: string - website: string | null - pinned: boolean - color: string | null - username: string - avatar_url: string | null - avatar: string | null - user_id: number - created_at: string - author?: { username: string; avatar: string | null } - user?: { username: string; avatar: string | null } - files?: NoteFile[] -} - -interface NoteAuthor { - username: string - avatar?: string | null -} - -const FONT = "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" - -// ── Website Thumbnail (fetches OG image) ──────────────────────────────────── -const ogCache = {} - -interface WebsiteThumbnailProps { - url: string - tripId: number - color: string -} - -function WebsiteThumbnail({ url, tripId, color }: WebsiteThumbnailProps) { - const [data, setData] = useState(ogCache[url] || null) - const [failed, setFailed] = useState(false) - - useEffect(() => { - if (ogCache[url]) { setData(ogCache[url]); return } - collabApi.linkPreview(tripId, url).then(d => { ogCache[url] = d; setData(d) }).catch(() => setFailed(true)) - }, [url, tripId]) - - const domain = (() => { try { return new URL(url).hostname.replace('www.', '') } catch { return 'link' } })() - - return ( - { e.currentTarget.style.transform = 'scale(1.08)'; e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)' }} - onMouseLeave={e => { e.currentTarget.style.transform = 'scale(1)'; e.currentTarget.style.boxShadow = 'none' }}> - {data?.image && !failed ? ( - setFailed(true)} /> - ) : ( - <> - - - {domain} - - - )} - - ) -} - -// ── File Preview Portal ───────────────────────────────────────────────────── -interface FilePreviewPortalProps { - file: NoteFile | null - onClose: () => void -} - -function FilePreviewPortal({ file, onClose }: FilePreviewPortalProps) { - const [authUrl, setAuthUrl] = useState('') - const rawUrl = file?.url || '' - useEffect(() => { - setAuthUrl('') - if (!rawUrl) return - getAuthUrl(rawUrl, 'download').then(setAuthUrl) - }, [rawUrl]) - - if (!file) return null - const isImage = file.mime_type?.startsWith('image/') - const isPdf = file.mime_type === 'application/pdf' - const isTxt = file.mime_type?.startsWith('text/') - - const openInNewTab = () => openFile(rawUrl).catch(() => {}) - - return ReactDOM.createPortal( -
- {isImage ? ( - /* Image lightbox — floating controls */ -
e.stopPropagation()}> - {authUrl - ? {file.original_name} - : - } -
- {file.original_name} -
- - -
-
-
- ) : ( - /* Document viewer — card with header */ -
e.stopPropagation()}> -
- {file.original_name} -
- - -
-
- {(isPdf || isTxt) ? ( - -

- -

-
- ) : ( -
- -
- )} -
- )} -
, - document.body - ) -} - -function AuthedImg({ src, style, onClick, onMouseEnter, onMouseLeave, alt }: { src: string; style?: React.CSSProperties; onClick?: () => void; onMouseEnter?: React.MouseEventHandler; onMouseLeave?: React.MouseEventHandler; alt?: string }) { - const [authSrc, setAuthSrc] = useState('') - useEffect(() => { - getAuthUrl(src, 'download').then(setAuthSrc) - }, [src]) - return authSrc ? {alt} : null -} - -const NOTE_COLORS = [ - { value: '#6366f1', label: 'Indigo' }, - { value: '#ef4444', label: 'Red' }, - { value: '#f59e0b', label: 'Amber' }, - { value: '#10b981', label: 'Emerald' }, - { value: '#3b82f6', label: 'Blue' }, - { value: '#8b5cf6', label: 'Violet' }, -] - -const formatTimestamp = (ts, t, locale) => { - if (!ts) return '' - const d = new Date(ts.endsWith?.('Z') ? ts : ts + 'Z') - const now = new Date() - const diffMs = now - d - const diffMins = Math.floor(diffMs / 60000) - if (diffMins < 1) return t('collab.chat.justNow') || 'just now' - if (diffMins < 60) return t('collab.chat.minutesAgo', { n: diffMins }) || `${diffMins}m ago` - const diffHrs = Math.floor(diffMins / 60) - if (diffHrs < 24) return t('collab.chat.hoursAgo', { n: diffHrs }) || `${diffHrs}h ago` - const diffDays = Math.floor(diffHrs / 24) - if (diffDays < 7) return t('collab.notes.daysAgo', { n: diffDays }) || `${diffDays}d ago` - return d.toLocaleDateString(locale || undefined, { month: 'short', day: 'numeric' }) -} - -// ── Avatar ────────────────────────────────────────────────────────────────── -interface UserAvatarProps { - user: NoteAuthor | null - size?: number -} - -function UserAvatar({ user, size = 14 }: UserAvatarProps) { - if (!user) return null - if (user.avatar) { - return ( - {user.username} - ) - } - const initials = (user.username || '?').slice(0, 1) - return ( -
- {initials} -
- ) -} - -// ── New Note Modal (portal to body) ───────────────────────────────────────── -interface NoteFormModalProps { - onClose: () => void - onSubmit: (data: { title: string; content: string; category: string; website: string; files?: File[] }) => Promise - onDeleteFile?: (noteId: number, fileId: number) => Promise - existingCategories: string[] - categoryColors: Record - getCategoryColor: (category: string) => string - note: CollabNote | null - tripId: number - t: (key: string) => string -} - -function NoteFormModal({ onClose, onSubmit, onDeleteFile, existingCategories, categoryColors, getCategoryColor, note, tripId, t }: NoteFormModalProps) { - const can = useCanDo() - const tripObj = useTripStore((s) => s.trip) - const canUploadFiles = can('file_upload', tripObj) - const isEdit = !!note - const allCategories = [...new Set([...existingCategories, ...Object.keys(categoryColors || {})])].filter(Boolean) - - const [title, setTitle] = useState(note?.title || '') - const [content, setContent] = useState(note?.content || '') - const [category, setCategory] = useState(note?.category || allCategories[0] || '') - const [website, setWebsite] = useState(note?.website || '') - const [pendingFiles, setPendingFiles] = useState([]) - const [existingAttachments, setExistingAttachments] = useState(note?.attachments || []) - const [submitting, setSubmitting] = useState(false) - const fileRef = useRef(null) - - const finalCategory = category - - const handleSubmit = async (e) => { - e.preventDefault() - if (!title.trim()) return - setSubmitting(true) - try { - await onSubmit({ - title: title.trim(), - content: content.trim(), - category: finalCategory || null, - color: getCategoryColor(finalCategory), - website: website.trim() || null, - _pendingFiles: pendingFiles, - }) - onClose() - } catch { - } finally { - setSubmitting(false) - } - } - - const handleDeleteAttachment = async (fileId) => { - if (onDeleteFile && note) { - await onDeleteFile(note.id, fileId) - setExistingAttachments(prev => prev.filter(a => a.id !== fileId)) - } - } - - const canSubmit = title.trim() && !submitting - - return ReactDOM.createPortal( -
-
e.stopPropagation()} - onPaste={e => { - if (!canUploadFiles) return - const items = e.clipboardData?.items - if (!items) return - for (const item of Array.from(items)) { - if (item.type.startsWith('image/') || item.type === 'application/pdf') { - e.preventDefault() - const file = item.getAsFile() - if (file) setPendingFiles(prev => [...prev, file]) - return - } - } - }} - onSubmit={handleSubmit} - > - {/* Modal header */} -
-

- {isEdit ? t('collab.notes.edit') : t('collab.notes.new')} -

- -
- - {/* Modal body */} -
- {/* Title */} -
-
- {t('collab.notes.title')} -
- setTitle(e.target.value)} - placeholder={t('collab.notes.titlePlaceholder')} - style={{ - width: '100%', - border: '1px solid var(--border-primary)', - borderRadius: 10, - padding: '8px 12px', - fontSize: 13, - background: 'var(--bg-input)', - color: 'var(--text-primary)', - fontFamily: 'inherit', - outline: 'none', - boxSizing: 'border-box', - }} - /> -
- - {/* Content */} -
-
- {t('collab.notes.contentPlaceholder')} -
-