From 9706fcc520672f5b9788f627789471ea094e2126 Mon Sep 17 00:00:00 2001 From: abhishek Date: Mon, 28 Sep 2026 17:24:00 +0530 Subject: [PATCH] initial commit --- .dockerignore | 31 + .env | 13 + .env.example | 5 + .gitignore | 15 + .npmrc | 1 + Dockerfile | 131 + README.md | 145 + index.html | 17 + nginx.conf.template | 183 + package-lock.json | 5160 +++++++++++++++++ package.json | 49 + public/favicon.ico | Bin 0 -> 11108 bytes public/favicon.png | Bin 0 -> 2008 bytes public/icon-16.png | Bin 0 -> 803 bytes public/icon-180.png | Bin 0 -> 17329 bytes public/icon-192.png | Bin 0 -> 18637 bytes public/icon-32.png | Bin 0 -> 2008 bytes public/icon-48.png | Bin 0 -> 3327 bytes public/icon-512.png | Bin 0 -> 31096 bytes public/logo-512.png | Bin 0 -> 31096 bytes public/logo-wordmark-light.png | Bin 0 -> 22027 bytes public/logo-wordmark.png | Bin 0 -> 22191 bytes scripts/appfix.mjs | 262 + scripts/appgap.mjs | 193 + scripts/appsweep.mjs | 246 + scripts/checkHealthPanel.ts | 91 + scripts/checkOptimiser.ts | 89 + scripts/contract.mjs | 434 ++ scripts/db.mjs | 399 ++ scripts/mapPreview.mjs | 167 + scripts/refileCategories.ts | 211 + src/App.tsx | 111 + src/api/assistant.ts | 117 + src/api/catalogue.ts | 137 + src/api/catalogueKey.test.ts | 85 + src/api/client.ts | 305 + src/api/customers.ts | 92 + src/api/deliveries.ts | 490 ++ src/api/ingest.test.ts | 370 ++ src/api/ingest.ts | 819 +++ src/api/insights.ts | 262 + src/api/nutrition.ts | 166 + src/api/optimiser.ts | 244 + src/api/people.ts | 252 + src/api/products.ts | 435 ++ src/api/routing.ts | 135 + src/api/stock.ts | 193 + src/api/telemetry.ts | 112 + src/api/tenants.ts | 319 + src/api/types.ts | 1003 ++++ src/api/uploads.test.ts | 100 + src/api/uploads.ts | 220 + src/auth/AuthContext.tsx | 52 + src/auth/context.ts | 45 + src/auth/roles.test.ts | 58 + src/auth/roles.ts | 128 + src/auth/session.test.ts | 113 + src/auth/session.ts | 298 + src/auth/token.ts | 81 + src/auth/workspace.test.ts | 59 + src/auth/workspace.ts | 69 + src/components/DataState.tsx | 65 + src/components/ErrorBoundary.tsx | 139 + src/components/Freshness.tsx | 53 + src/components/KpiCard.tsx | 89 + src/components/PageBody.tsx | 34 + src/components/PageHeader.tsx | 107 + src/components/Panel.tsx | 139 + src/components/SearchInput.tsx | 70 + src/components/SectionHeader.tsx | 39 + src/components/SheetDropzone.tsx | 235 + src/components/StickyRow.tsx | 59 + src/components/TabBar.tsx | 100 + src/components/TablePager.tsx | 49 + src/components/TrailMap.tsx | 436 ++ src/components/kpiCard.css | 140 + src/components/panel.css | 141 + src/components/searchInput.css | 82 + src/components/shell/AppShell.tsx | 824 +++ src/components/shell/AssistantPanel.tsx | 1009 ++++ src/components/shell/DateScope.tsx | 131 + src/components/shell/assistantContext.test.ts | 91 + src/components/shell/assistantContext.ts | 280 + src/components/shell/assistantScope.ts | 21 + src/components/shell/assistantWidth.ts | 94 + src/components/shell/composerState.test.ts | 70 + src/components/tabBar.css | 165 + src/components/tablePager.css | 24 + src/components/trailMap.css | 406 ++ src/components/usePaged.test.ts | 104 + src/components/usePaged.ts | 131 + src/components/useSelection.test.ts | 60 + src/components/useSelection.ts | 88 + src/env.d.ts | 9 + src/features/auth/LoginPage.tsx | 946 +++ src/features/catalogue/CatalogueBrowser.tsx | 603 ++ src/features/catalogue/CatalogueCard.tsx | 251 + .../catalogue/CatalogueDetailDrawer.tsx | 312 + src/features/catalogue/CatalogueSidebar.tsx | 158 + src/features/catalogue/tenantCategories.ts | 25 + .../nearle-admin/NearleAdminShell.tsx | 63 + .../nearle-admin/PartnerAssignDrawer.tsx | 204 + .../nearle-admin/PartnerRidersDrawer.tsx | 135 + src/features/nearle-admin/ShiftsPanel.tsx | 373 ++ .../nearle-admin/import/ImportScope.tsx | 149 + .../nearle-admin/import/SheetImportPanel.tsx | 989 ++++ .../nearle-admin/import/openingStock.test.ts | 268 + .../nearle-admin/import/openingStock.ts | 235 + .../import/parseProductSheet.test.ts | 77 + .../nearle-admin/import/parseProductSheet.ts | 296 + src/features/nearle-admin/import/shelve.ts | 133 + .../nearle-admin/pages/DispatchPage.tsx | 135 + .../pages/GlobalCataloguePage.tsx | 30 + .../nearle-admin/pages/OnboardTenantPage.tsx | 438 ++ .../nearle-admin/pages/PartnersPage.tsx | 698 +++ .../nearle-admin/pages/StoreDetailPage.tsx | 340 ++ .../nearle-admin/pages/StoresPage.tsx | 454 ++ .../nearle-admin/pages/UploadsPage.tsx | 39 + src/features/nearle-admin/pages/fleet.css | 293 + src/features/nearle-admin/partnerList.test.ts | 73 + src/features/nearle-admin/partnerList.ts | 51 + src/features/nearle-admin/riderShifts.test.ts | 251 + src/features/nearle-admin/riderShifts.ts | 324 ++ .../nearle-admin/tamilNaduDistricts.test.ts | 74 + .../nearle-admin/tamilNaduDistricts.ts | 133 + src/features/qr/StoreQrPanel.tsx | 163 + src/features/qr/qr.test.ts | 138 + src/features/qr/qr.ts | 537 ++ src/features/store-admin/ActivePanel.tsx | 236 + src/features/store-admin/AssignBar.tsx | 279 + src/features/store-admin/AutoAssignDrawer.tsx | 362 ++ .../store-admin/BranchScope.dom.test.tsx | 261 + src/features/store-admin/BranchScope.tsx | 184 + src/features/store-admin/DateRangePicker.tsx | 115 + src/features/store-admin/DeliveryProgress.tsx | 158 + src/features/store-admin/Drawer.tsx | 168 + .../store-admin/GroupMap.dom.test.tsx | 286 + src/features/store-admin/GroupMap.tsx | 260 + src/features/store-admin/HealthScorePanel.tsx | 224 + .../store-admin/OrderDetailDrawer.tsx | 569 ++ .../store-admin/PlanVsActualPanel.tsx | 339 ++ src/features/store-admin/ProfitPanel.tsx | 254 + src/features/store-admin/RiderDrawer.tsx | 430 ++ src/features/store-admin/RoutePlanDrawer.tsx | 350 ++ src/features/store-admin/appAisle.test.ts | 126 + src/features/store-admin/appAisle.ts | 239 + .../store-admin/assignDelivery.test.ts | 413 ++ src/features/store-admin/assignDelivery.ts | 422 ++ src/features/store-admin/autoAssign.test.ts | 338 ++ src/features/store-admin/autoAssign.ts | 350 ++ src/features/store-admin/branchStats.test.ts | 165 + src/features/store-admin/branchStats.ts | 140 + src/features/store-admin/dateRangePicker.css | 38 + .../store-admin/deliveryPhase.test.ts | 64 + .../store-admin/deliveryTrack.test.ts | 243 + src/features/store-admin/deliveryTrack.ts | 242 + .../store-admin/dispatchModel.test.ts | 409 ++ src/features/store-admin/dispatchModel.ts | 483 ++ src/features/store-admin/dispatchPanels.css | 531 ++ .../store-admin/dispatchScope.test.ts | 121 + src/features/store-admin/dispatchScope.ts | 86 + src/features/store-admin/drawer.css | 917 +++ src/features/store-admin/drawerKit.tsx | 435 ++ src/features/store-admin/format.test.ts | 35 + src/features/store-admin/format.ts | 103 + src/features/store-admin/healthScore.test.ts | 305 + src/features/store-admin/healthScore.ts | 259 + src/features/store-admin/orderItems.test.ts | 90 + src/features/store-admin/orderItems.ts | 77 + .../store-admin/orderProgress.test.ts | 136 + src/features/store-admin/orderProgress.ts | 146 + src/features/store-admin/orderStatus.test.ts | 297 + src/features/store-admin/orderStatus.ts | 385 ++ .../store-admin/pages/DispatchPage.tsx | 1014 ++++ src/features/store-admin/pages/deliveries.css | 357 ++ src/features/store-admin/pages/dispatch.css | 769 +++ .../store-admin/plannedVsActual.test.ts | 250 + src/features/store-admin/plannedVsActual.ts | 298 + src/features/store-admin/posStatus.ts | 348 ++ .../store-admin/productCategory.test.ts | 252 + src/features/store-admin/productCategory.ts | 385 ++ .../store-admin/profitability.test.ts | 179 + src/features/store-admin/profitability.ts | 235 + src/features/store-admin/riderLive.test.ts | 208 + src/features/store-admin/riderLive.ts | 233 + src/features/store-admin/riderReach.test.ts | 157 + src/features/store-admin/riderReach.ts | 166 + src/features/store-admin/routePlan.test.ts | 201 + src/features/store-admin/routePlan.ts | 230 + src/features/store-admin/useRoads.ts | 52 + src/features/uploads/UploadDetailDrawer.tsx | 443 ++ src/features/uploads/UploadsPanel.tsx | 685 +++ src/features/uploads/shelveRows.test.ts | 50 + src/features/uploads/shelveRows.ts | 23 + src/features/uploads/uploadState.test.ts | 262 + src/features/uploads/uploadState.ts | 281 + src/features/uploads/uploads.css | 226 + src/hooks/useIsMobile.ts | 45 + src/index.css | 2382 ++++++++ src/lib/staleChunk.test.ts | 112 + src/lib/staleChunk.ts | 143 + src/main.tsx | 56 + src/queries/hooks.ts | 787 +++ src/queries/interval.ts | 9 + src/queries/keys.ts | 125 + src/theme/nearle.css | 141 + src/theme/nearle.d.ts | 10 + src/theme/nearle.js | 72 + src/theme/nearle.ts | 152 + tools/css-hooks.mjs | 19 + tools/stub-css.mjs | 9 + tsconfig.json | 25 + vite.config.ts | 97 + 213 files changed, 53142 insertions(+) create mode 100644 .dockerignore create mode 100644 .env create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 index.html create mode 100644 nginx.conf.template create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/favicon.ico create mode 100644 public/favicon.png create mode 100644 public/icon-16.png create mode 100644 public/icon-180.png create mode 100644 public/icon-192.png create mode 100644 public/icon-32.png create mode 100644 public/icon-48.png create mode 100644 public/icon-512.png create mode 100644 public/logo-512.png create mode 100644 public/logo-wordmark-light.png create mode 100644 public/logo-wordmark.png create mode 100644 scripts/appfix.mjs create mode 100644 scripts/appgap.mjs create mode 100644 scripts/appsweep.mjs create mode 100644 scripts/checkHealthPanel.ts create mode 100644 scripts/checkOptimiser.ts create mode 100644 scripts/contract.mjs create mode 100644 scripts/db.mjs create mode 100644 scripts/mapPreview.mjs create mode 100644 scripts/refileCategories.ts create mode 100644 src/App.tsx create mode 100644 src/api/assistant.ts create mode 100644 src/api/catalogue.ts create mode 100644 src/api/catalogueKey.test.ts create mode 100644 src/api/client.ts create mode 100644 src/api/customers.ts create mode 100644 src/api/deliveries.ts create mode 100644 src/api/ingest.test.ts create mode 100644 src/api/ingest.ts create mode 100644 src/api/insights.ts create mode 100644 src/api/nutrition.ts create mode 100644 src/api/optimiser.ts create mode 100644 src/api/people.ts create mode 100644 src/api/products.ts create mode 100644 src/api/routing.ts create mode 100644 src/api/stock.ts create mode 100644 src/api/telemetry.ts create mode 100644 src/api/tenants.ts create mode 100644 src/api/types.ts create mode 100644 src/api/uploads.test.ts create mode 100644 src/api/uploads.ts create mode 100644 src/auth/AuthContext.tsx create mode 100644 src/auth/context.ts create mode 100644 src/auth/roles.test.ts create mode 100644 src/auth/roles.ts create mode 100644 src/auth/session.test.ts create mode 100644 src/auth/session.ts create mode 100644 src/auth/token.ts create mode 100644 src/auth/workspace.test.ts create mode 100644 src/auth/workspace.ts create mode 100644 src/components/DataState.tsx create mode 100644 src/components/ErrorBoundary.tsx create mode 100644 src/components/Freshness.tsx create mode 100644 src/components/KpiCard.tsx create mode 100644 src/components/PageBody.tsx create mode 100644 src/components/PageHeader.tsx create mode 100644 src/components/Panel.tsx create mode 100644 src/components/SearchInput.tsx create mode 100644 src/components/SectionHeader.tsx create mode 100644 src/components/SheetDropzone.tsx create mode 100644 src/components/StickyRow.tsx create mode 100644 src/components/TabBar.tsx create mode 100644 src/components/TablePager.tsx create mode 100644 src/components/TrailMap.tsx create mode 100644 src/components/kpiCard.css create mode 100644 src/components/panel.css create mode 100644 src/components/searchInput.css create mode 100644 src/components/shell/AppShell.tsx create mode 100644 src/components/shell/AssistantPanel.tsx create mode 100644 src/components/shell/DateScope.tsx create mode 100644 src/components/shell/assistantContext.test.ts create mode 100644 src/components/shell/assistantContext.ts create mode 100644 src/components/shell/assistantScope.ts create mode 100644 src/components/shell/assistantWidth.ts create mode 100644 src/components/shell/composerState.test.ts create mode 100644 src/components/tabBar.css create mode 100644 src/components/tablePager.css create mode 100644 src/components/trailMap.css create mode 100644 src/components/usePaged.test.ts create mode 100644 src/components/usePaged.ts create mode 100644 src/components/useSelection.test.ts create mode 100644 src/components/useSelection.ts create mode 100644 src/env.d.ts create mode 100644 src/features/auth/LoginPage.tsx create mode 100644 src/features/catalogue/CatalogueBrowser.tsx create mode 100644 src/features/catalogue/CatalogueCard.tsx create mode 100644 src/features/catalogue/CatalogueDetailDrawer.tsx create mode 100644 src/features/catalogue/CatalogueSidebar.tsx create mode 100644 src/features/catalogue/tenantCategories.ts create mode 100644 src/features/nearle-admin/NearleAdminShell.tsx create mode 100644 src/features/nearle-admin/PartnerAssignDrawer.tsx create mode 100644 src/features/nearle-admin/PartnerRidersDrawer.tsx create mode 100644 src/features/nearle-admin/ShiftsPanel.tsx create mode 100644 src/features/nearle-admin/import/ImportScope.tsx create mode 100644 src/features/nearle-admin/import/SheetImportPanel.tsx create mode 100644 src/features/nearle-admin/import/openingStock.test.ts create mode 100644 src/features/nearle-admin/import/openingStock.ts create mode 100644 src/features/nearle-admin/import/parseProductSheet.test.ts create mode 100644 src/features/nearle-admin/import/parseProductSheet.ts create mode 100644 src/features/nearle-admin/import/shelve.ts create mode 100644 src/features/nearle-admin/pages/DispatchPage.tsx create mode 100644 src/features/nearle-admin/pages/GlobalCataloguePage.tsx create mode 100644 src/features/nearle-admin/pages/OnboardTenantPage.tsx create mode 100644 src/features/nearle-admin/pages/PartnersPage.tsx create mode 100644 src/features/nearle-admin/pages/StoreDetailPage.tsx create mode 100644 src/features/nearle-admin/pages/StoresPage.tsx create mode 100644 src/features/nearle-admin/pages/UploadsPage.tsx create mode 100644 src/features/nearle-admin/pages/fleet.css create mode 100644 src/features/nearle-admin/partnerList.test.ts create mode 100644 src/features/nearle-admin/partnerList.ts create mode 100644 src/features/nearle-admin/riderShifts.test.ts create mode 100644 src/features/nearle-admin/riderShifts.ts create mode 100644 src/features/nearle-admin/tamilNaduDistricts.test.ts create mode 100644 src/features/nearle-admin/tamilNaduDistricts.ts create mode 100644 src/features/qr/StoreQrPanel.tsx create mode 100644 src/features/qr/qr.test.ts create mode 100644 src/features/qr/qr.ts create mode 100644 src/features/store-admin/ActivePanel.tsx create mode 100644 src/features/store-admin/AssignBar.tsx create mode 100644 src/features/store-admin/AutoAssignDrawer.tsx create mode 100644 src/features/store-admin/BranchScope.dom.test.tsx create mode 100644 src/features/store-admin/BranchScope.tsx create mode 100644 src/features/store-admin/DateRangePicker.tsx create mode 100644 src/features/store-admin/DeliveryProgress.tsx create mode 100644 src/features/store-admin/Drawer.tsx create mode 100644 src/features/store-admin/GroupMap.dom.test.tsx create mode 100644 src/features/store-admin/GroupMap.tsx create mode 100644 src/features/store-admin/HealthScorePanel.tsx create mode 100644 src/features/store-admin/OrderDetailDrawer.tsx create mode 100644 src/features/store-admin/PlanVsActualPanel.tsx create mode 100644 src/features/store-admin/ProfitPanel.tsx create mode 100644 src/features/store-admin/RiderDrawer.tsx create mode 100644 src/features/store-admin/RoutePlanDrawer.tsx create mode 100644 src/features/store-admin/appAisle.test.ts create mode 100644 src/features/store-admin/appAisle.ts create mode 100644 src/features/store-admin/assignDelivery.test.ts create mode 100644 src/features/store-admin/assignDelivery.ts create mode 100644 src/features/store-admin/autoAssign.test.ts create mode 100644 src/features/store-admin/autoAssign.ts create mode 100644 src/features/store-admin/branchStats.test.ts create mode 100644 src/features/store-admin/branchStats.ts create mode 100644 src/features/store-admin/dateRangePicker.css create mode 100644 src/features/store-admin/deliveryPhase.test.ts create mode 100644 src/features/store-admin/deliveryTrack.test.ts create mode 100644 src/features/store-admin/deliveryTrack.ts create mode 100644 src/features/store-admin/dispatchModel.test.ts create mode 100644 src/features/store-admin/dispatchModel.ts create mode 100644 src/features/store-admin/dispatchPanels.css create mode 100644 src/features/store-admin/dispatchScope.test.ts create mode 100644 src/features/store-admin/dispatchScope.ts create mode 100644 src/features/store-admin/drawer.css create mode 100644 src/features/store-admin/drawerKit.tsx create mode 100644 src/features/store-admin/format.test.ts create mode 100644 src/features/store-admin/format.ts create mode 100644 src/features/store-admin/healthScore.test.ts create mode 100644 src/features/store-admin/healthScore.ts create mode 100644 src/features/store-admin/orderItems.test.ts create mode 100644 src/features/store-admin/orderItems.ts create mode 100644 src/features/store-admin/orderProgress.test.ts create mode 100644 src/features/store-admin/orderProgress.ts create mode 100644 src/features/store-admin/orderStatus.test.ts create mode 100644 src/features/store-admin/orderStatus.ts create mode 100644 src/features/store-admin/pages/DispatchPage.tsx create mode 100644 src/features/store-admin/pages/deliveries.css create mode 100644 src/features/store-admin/pages/dispatch.css create mode 100644 src/features/store-admin/plannedVsActual.test.ts create mode 100644 src/features/store-admin/plannedVsActual.ts create mode 100644 src/features/store-admin/posStatus.ts create mode 100644 src/features/store-admin/productCategory.test.ts create mode 100644 src/features/store-admin/productCategory.ts create mode 100644 src/features/store-admin/profitability.test.ts create mode 100644 src/features/store-admin/profitability.ts create mode 100644 src/features/store-admin/riderLive.test.ts create mode 100644 src/features/store-admin/riderLive.ts create mode 100644 src/features/store-admin/riderReach.test.ts create mode 100644 src/features/store-admin/riderReach.ts create mode 100644 src/features/store-admin/routePlan.test.ts create mode 100644 src/features/store-admin/routePlan.ts create mode 100644 src/features/store-admin/useRoads.ts create mode 100644 src/features/uploads/UploadDetailDrawer.tsx create mode 100644 src/features/uploads/UploadsPanel.tsx create mode 100644 src/features/uploads/shelveRows.test.ts create mode 100644 src/features/uploads/shelveRows.ts create mode 100644 src/features/uploads/uploadState.test.ts create mode 100644 src/features/uploads/uploadState.ts create mode 100644 src/features/uploads/uploads.css create mode 100644 src/hooks/useIsMobile.ts create mode 100644 src/index.css create mode 100644 src/lib/staleChunk.test.ts create mode 100644 src/lib/staleChunk.ts create mode 100644 src/main.tsx create mode 100644 src/queries/hooks.ts create mode 100644 src/queries/interval.ts create mode 100644 src/queries/keys.ts create mode 100644 src/theme/nearle.css create mode 100644 src/theme/nearle.d.ts create mode 100644 src/theme/nearle.js create mode 100644 src/theme/nearle.ts create mode 100644 tools/css-hooks.mjs create mode 100644 tools/stub-css.mjs create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4319b16 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,31 @@ +# What must never enter the build context. +# +# There was no .dockerignore at all, so every one of these was being uploaded to +# the Docker daemon on each build and then copied into the builder image by +# `COPY . .`. +# +# `.git` is the one that matters for speed — it carries the entire history, and +# it grows forever while being worthless to a build. The rest is correctness: +# `dist` and `node_modules` from a developer's machine would be copied in and +# then overwritten by the build, but only after being transferred, and a stale +# `dist` copied over the fresh one is a genuinely confusing failure. +.git +.gitignore +node_modules +dist + +# Transfer archives. The scratch files these rules were written for — the +# `_to_delete` directory, `_inv.txt`, and two empty archives — are gone from the +# repository; the glob stays so the next one never reaches a build context. +*.zip +*.tgz + +# Per-machine overrides. `.env` itself IS wanted — it carries VITE_API_BASE and +# the build needs it — but `.env.local` is a developer's private override and +# must not decide what production talks to. +.env.local +*.local + +.vscode +.DS_Store +README.md diff --git a/.env b/.env new file mode 100644 index 0000000..6848860 --- /dev/null +++ b/.env @@ -0,0 +1,13 @@ +# The Fiesta API host. Read by Vite at build time and compiled into the bundle. +# +# This is the single source of truth for where the console talks to the backend, +# and it is not a secret — it is the same public host the customer app calls. +# +# Committed on purpose: a deployed build has to carry it, and a value that lives +# only on one developer's machine is one the build server does not have. Vite +# only exposes `VITE_`-prefixed names to client code, so nothing else in here +# would reach the browser. +# +# `.env.local` overrides this and is gitignored — that is the file to use for a +# staging backend, or `/fiesta` to route through the dev proxy instead. +VITE_API_BASE="https://fiesta.nearle.app" diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dd616c6 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# The Fiesta API host used by the production build. +# In development this is left unset and Vite proxies /fiesta -> fiesta.nearle.app +# (see vite.config.ts), which keeps the network tab consistent and avoids +# preflight surprises. +VITE_API_BASE="https://fiesta.nearle.app" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c6e3e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +node_modules +dist + +*.local +.DS_Store +.vscode + +# Transfer archives. Nothing of the sort belongs in the repository — the two +# that were here (`_sync.zip`, `_their-src.tgz`) had been committed empty and +# stayed for weeks. +*.zip +*.tgz + +# Build output from scripts/mapPreview.mjs — a local viewer, never committed. +scripts/.preview/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..c0c80ba --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=false diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..be0e870 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,131 @@ +# Stage 1 — build +FROM node:22-alpine AS builder + +WORKDIR /app + +COPY package*.json ./ + +# `npm ci`, with no `|| npm install` fallback. +# +# That fallback was here and it is worse than the error it hides. `npm ci` +# fails only when package-lock.json disagrees with package.json — exactly the +# case where falling back to `npm install` resolves fresh versions the lock file +# never pinned, so the deployed bundle is built from dependencies nobody chose +# and nobody can reproduce. +# +# When this line fails, the fix is to update package-lock.json locally and +# COMMIT it. The build should not paper over a lock file that is out of date; +# it should say so. +# +# ── But `npm install` alone is how the lock got broken once ───────────────── +# +# This image is node:22-alpine, which ships npm 10.9.8. A developer on npm 11 +# running `npm install` rewrites the lock in a shape npm 10 rejects: npm 11 +# prunes optional platform packages that npm 10 still validates. Adding leaflet +# on npm 11.6.2 dropped `@emnapi/core` and `@emnapi/runtime` — transitive +# optional deps of `@tailwindcss/oxide-wasm32-wasi` — and the next deploy died +# here with "Missing: @emnapi/core@1.11.3 from lock file". Nothing was wrong +# with the code; the lock was genuinely incomplete and this line was right to +# refuse it. +# +# So after changing dependencies, prove the lock against THIS npm before +# pushing, in a scratch directory so node_modules is not disturbed: +# +# mkdir /tmp/lockcheck && cp package.json package-lock.json /tmp/lockcheck/ +# cd /tmp/lockcheck && npx npm@10.9.8 ci +# +# and if it fails, regenerate with the same version: +# +# npx npm@10.9.8 install --package-lock-only +RUN npm ci --no-audit --no-fund + +COPY . . + +# Where the bundle points at the backend, fixed HERE rather than left to `.env`. +# +# Deployment platforms write their own `.env` into the source directory before +# building — Dokploy does — which overwrites the committed one and takes +# VITE_API_BASE with it. The build then fell through to a same-origin path and +# the console called `https:///fiesta/live/api/...`. A build +# argument outranks the file, so the host survives that overwrite. +# +# Override per environment with `--build-arg VITE_API_BASE=...` (Dokploy: Build +# Args), e.g. to point a staging console at a staging Fiesta. +ARG VITE_API_BASE="https://fiesta.nearle.app" +ENV VITE_API_BASE=$VITE_API_BASE + +# There is no workspace flag in this image. +# +# This repository IS the Nearle platform console — every route it mounts is the +# platform workspace and only Nearle staff can sign in. The merchant console is +# a separate repository with its own deploy. A flag here would exist only as a +# way to deploy this application as something it is not. +# +# The merchant console's address, for the sentence shown to a merchant who +# signs in at the wrong site. A build argument rather than a constant because +# the two are separate deployments and either can move. +ARG VITE_MERCHANT_HOST="app.nearledaily.com" +ENV VITE_MERCHANT_HOST=$VITE_MERCHANT_HOST + +RUN npm run build + +# Stage 2 — serve +FROM nginx:alpine + +# The config is a TEMPLATE, and that is deliberate. +# +# nginx:alpine's entrypoint runs `envsubst` over /etc/nginx/templates/*.template +# at container start and writes the result into conf.d. That is how the ingest +# API key reaches nginx as a runtime environment variable rather than being +# committed here in plain text — which is what the previous Dockerfile did with +# the Hasura secret, on the line this replaces. +COPY nginx.conf.template /etc/nginx/templates/default.conf.template + +# Restricts substitution to this one name. +# +# Without the filter, envsubst replaces every `${...}` it recognises as an +# environment variable — and the container's environment carries HOSTNAME, PATH +# and friends. Nginx's own `$uri`, `$remote_addr` and `$proxy_add_x_forwarded_for` +# would survive that today, but only by luck, and a config silently rewritten at +# boot is a bad thing to leave to luck. +ENV NGINX_ENVSUBST_FILTER="(INGEST_TOKEN|INGEST_UPSTREAM)" + +# Empty by default — and this line is LOAD-BEARING. Do not delete it. +# +# BuildKit warns about it: `SecretsUsedInArgOrEnv: Do not use ARG or ENV +# instructions for sensitive data (ENV "INGEST_TOKEN")`. The warning is right in +# general and wrong here: nothing sensitive is baked in, the value is the empty +# string, and the real token is supplied at RUN time by the deployment. +# +# Removing the line to silence the warning breaks the config in a way that is +# hard to see. nginx's entrypoint builds its substitution list from env vars +# that are DEFINED: +# +# defined_envs=$(printf '${%s} ' $(awk "END { for (name in ENVIRON) ... }")) +# +# With INGEST_TOKEN undefined, it is not in that list, envsubst leaves the +# placeholder alone, and nginx ends up with the literal text `${INGEST_TOKEN}` +# as the token — which is not empty, so the missing-token guard never fires and +# every ingest call goes out with a nonsense `X-API-Key`. +# +# Declaring it empty here guarantees envsubst always substitutes it, so an +# unset token is a real empty string and the guard can catch it. +ENV INGEST_TOKEN="" + +# Where the ingest service is. Declared here for the same reason as the line +# above: envsubst only substitutes names that are DEFINED, so an undeclared +# INGEST_UPSTREAM would leave the literal text `${INGEST_UPSTREAM}` in the +# config as the proxy target, and nginx would fail to start with an error that +# names the variable rather than the omission. +# +# Defaults to the public host so an existing deployment behaves exactly as it +# did. Set it to the sibling container's internal address — e.g. +# `http://mcp-backend:8000` — to take the private path and drop the credential +# entirely. +ENV INGEST_UPSTREAM="https://mcp.nearle.ai.in" + +COPY --from=builder /app/dist/ /usr/share/nginx/html/ + +EXPOSE 80 3000 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..f267045 --- /dev/null +++ b/README.md @@ -0,0 +1,145 @@ +# Nearle Platform Console + +Nearle's own console, used by Nearle staff at **platform.nearledaily.com**: +the tenant directory, onboarding, the global catalogue, delivery partners and +platform-wide dispatch. Built against the existing Fiesta backend — the API is a +fixed constraint, not something this repo changes. + +## This is not the merchant console + +Merchants and their branch users sign in at **app.nearledaily.com**, which is a +separate repository (`nearle-console`) with its own deploy. Only `nearle-admin` +accounts can sign in here; a merchant account is refused with a sentence naming +where it belongs, and the refusal happens before any session is written. + +The two were one application with three workspaces behind a role guard, and were +split so an internal tool and a customer-facing product could move at their own +pace — and so a change made for staff could not reach a shop. + +**They share no code at runtime, and roughly thirty thousand lines are duplicated +between them**: the API layer, the query cache, the component library, the +drawers. That is a deliberate trade, taken because this side is internal — a +divergence here is something the team notices in its own tool rather than +something a merchant discovers. A fix worth having in both has to be made twice, +on purpose. + +Folders named `store-admin` remain under `src/features/`. They are not merchant +screens: they are the shared pieces this console depends on — the dispatch board, +the drawer kit, formatting, assignment logic — still carrying the name they had +before the split. + +## Running it + +```bash +npm install +npm run dev # http://localhost:3100 +npm run typecheck # tsc --noEmit +npm run build # typecheck + production build +``` + +> **Port 3100, not 3000.** The old console (`daily_merchant_web`) runs its dev +> server on 3000. `strictPort` is on, so if 3100 is taken this fails loudly +> rather than silently moving — which is the failure that makes you think your +> changes did not land when you are actually looking at a different app. + +In development Vite proxies `/fiesta` → `https://fiesta.nearle.app`. For a +deployed build set `VITE_API_BASE` (see `.env.example`). + +## Stack + +| Layer | Choice | +|---|---| +| Build | Vite 8 | +| Language | **TypeScript 7**, strict, `noUncheckedIndexedAccess`. No `.js`/`.jsx` anywhere. | +| UI | React 19 + **@astryxdesign/core** | +| Theme | `src/theme/nearle.ts`, compiled to `nearle.css` | +| Routing | React Router 7, split per page | +| Server state | TanStack Query 5 | +| Icons | lucide-react | +| Spreadsheets | `xlsx`, loaded on demand | + +## What's built + +All three workspaces. Which one opens is decided by the account, not chosen — +see `src/auth/roles.ts`. + +**Nearle Admin** (`issuperadmin`) — the platform operator: + +- `/nearle/stores` — every tenant, branch counts, per-tenant performance +- `/nearle/stores/:tenantId` — one tenant's branches, orders and revenue +- `/nearle/onboard/tenant` — provision a merchant group and its first outlet +- `/nearle/catalogue` — the global catalogue, plus both product-import paths +- `/nearle/partners` — rider partners, and the riders under each +- `/nearle/dispatch` — every partner's live work and shifts +- `/nearle/uploads` — spreadsheets sent to the catalogue service + +**Store Admin** (roleid 1 and 3) — one merchant, every branch: + +- `/admin/console` — online and counter sales side by side, per branch +- `/admin/sales` · `/admin/dispatch` · `/admin/inventory` · `/admin/reports` +- `/admin/branches/new` — commission an outlet with its delivery thresholds +- `/admin/users` — back-office people and till accounts +- `/admin/terminals` · `/admin/uploads` · `/admin/profile` · `/admin/onboarding` + +**Store user** (everything else) — one branch, scoped to it: + +- `/store/console` · `/store/products` · `/store/sales` · `/store/dispatch` +- `/store/reports` · `/store/customers` · `/store/terminals` · `/store/staff` +- `/store/uploads` · `/store/account` · `/store/setup` + +Two routes are redirects rather than pages, and deliberately: +`/nearle/onboard/branch` → `/nearle/stores` (a branch is commissioned from the +tenant that will own it), and `/nearle/fleet` → `/nearle/dispatch`. + +## Things about the backend that shape this code + +These are not bugs in this repo. They are the API's behaviour, and each one is +worked around deliberately. + +1. **No web authentication.** The login endpoints return the user record and no + token; only `/v1/pos/*` has middleware. So the session *is* that record, held + in `sessionStorage`. `src/auth/session.ts` is the only file that changes when + tokens arrive. +2. **Every list call needs a scoping id or it 400s.** The IDOR pass added + controller-level guards. Query hooks are therefore `enabled`-gated on the id + and the id is part of the query key. +3. **Roles are derived, not asserted.** `issuperadmin` is checked before + `roleid`, because the flag is server-derived and a roleid is not. Roleids 7 + and 8 are till roles and never reach an admin workspace. +4. **`POST /products/create` takes one product and returns no id.** The + controller passes the struct to the service by value, so GORM writes the + generated id into a copy that is then discarded. The sheet importer therefore + creates, then re-queries by SKU to resolve ids, then batches the location and + stock writes. See `importSheetProducts` in `src/api/products.ts`. +5. **The two import paths are not symmetric.** Catalogue import is one + idempotent batch call. Sheet import is N creates with no dedupe on SKU, so + re-uploading a file duplicates its products — the UI says so before the + button. +6. **The global catalogue has a price *range*, not a price**, and no mapping to + a tenant's own categories. Category, subcategory, retail price, cost and tax + are collected before an import can be enabled. +7. **POS endpoints take one required `locationid`.** There is no tenant-wide + counter-sales call, so a multi-branch POS view fans out per branch. + +## The 30-second cadence + +Online orders land when an order is placed; counter sales reach the console on a +30-second refetch; anything still sitting on an offline till has not arrived at +all. So **any figure blending the two is eventually consistent**, and the +`` component exists to say when a number was last true and how many +bills are still stranded. Polling is applied per query in `src/queries/hooks.ts` +rather than globally — a provisioning form has no business re-polling. + +## Design + +The visual system is ported from KROW (which is built on Astryx too, so the +tokens are role-for-role comparable), with Nearle purple `#662582` as the accent. +The ambient canvas in `index.css` — a fixed, viewport-wide horizontal gradient at +`z-index: -1` — is the one piece of KROW that carries most of the character. + +Radius, type scale and control heights are matched to KROW in +`src/theme/nearle.ts`, with the reasoning kept in comments. + +**Open:** the dark-mode accent `#B57FD0` was chosen for 5.41:1 contrast on +Astryx's dark surface and still needs brand-owner sign-off before dark mode +ships. diff --git a/index.html b/index.html new file mode 100644 index 0000000..5c6b8b7 --- /dev/null +++ b/index.html @@ -0,0 +1,17 @@ + + + + + + + + + + + Nearle Console + + +
+ + + diff --git a/nginx.conf.template b/nginx.conf.template new file mode 100644 index 0000000..9594d87 --- /dev/null +++ b/nginx.conf.template @@ -0,0 +1,183 @@ +# Nginx for the deployed console. +# +# A `.template`, not a plain conf: the nginx:alpine entrypoint runs `envsubst` +# over everything in /etc/nginx/templates and writes the result into conf.d at +# container start. That is what lets the ingest API key arrive as a runtime +# environment variable instead of being committed to this repository. +# +# ── Why this file exists in this shape ─────────────────────────────────────── +# +# The previous version was inherited from the old console and proxied `/hasura/` +# — a path this console never calls — while having no block for `/fiesta/` at +# all. Every API call therefore fell through to `try_files … /index.html`, and +# nginx answers a POST to a static file with **405 Method Not Allowed** and an +# HTML body. The console reported "Malformed response (HTTP 405)", which was +# accurate and pointed nowhere near the cause: sign-in was never reaching the +# backend. +# +# The rule this file follows: every prefix the Vite dev server proxies must have +# a matching block here. `vite.config.ts` is the other half of this file, and +# the two drift apart silently — it works on every developer machine and fails +# only once deployed. + +server { + listen 80; + listen 3000; + server_name _; + + # 10 MB is the ingest service's own file limit, so anything larger is going + # to be refused anyway — but nginx's default is 1 MB, and it rejects the + # upload itself with a 413 before the request ever leaves this container. + # A merchant's product sheet passes 1 MB easily. + client_max_body_size 12m; + + # ── The app ────────────────────────────────────────────────────────────── + location / { + root /usr/share/nginx/html; + index index.html; + # React Router owns the paths, so an unknown one is a route, not a 404. + try_files $uri $uri/ /index.html; + + # index.html must be revalidated on every visit, and until now it was + # not — the line below is new, and its absence was a real outage. + # + # The block above said "index.html must NOT be cached" and then set no + # cache header at all, which is not the same thing. With neither + # `Cache-Control` nor `Expires`, a browser falls back to HEURISTIC + # caching: RFC 9111 lets it invent a freshness lifetime from + # `Last-Modified`, commonly a tenth of the document's age, and serve the + # document from disk WITHOUT revalidating. So a tab kept the previous + # index.html, that index.html named `InventoryPage-BAzj-ICF.js`, the + # deploy had replaced it with `InventoryPage-Cbh53mHU.js`, and the + # import 404'd on a screen that had worked ten minutes earlier. + # + # `no-cache` does NOT mean "do not store" — it means "revalidate before + # use". The ETag still answers 304 on an unchanged deploy, so this costs + # one conditional request per visit and never a re-download. + add_header Cache-Control "no-cache" always; + } + + # Hashed filenames, so these can be cached hard — the hash changes when the + # content does, which is what makes a year safe. + location /assets/ { + root /usr/share/nginx/html; + + # ONE header, not two. `expires 1y` emits its own + # `Cache-Control: max-age=31536000`, and the `add_header` beside it + # appended a second, so every asset went out with two conflicting + # `Cache-Control` lines — `max-age=31536000` and `public, immutable`, + # neither complete. Browsers mostly cope; caches and CDNs in between are + # entitled to take the first and drop `immutable`, or to treat the pair + # as malformed. Merged into a single directive, with `expires` dropped + # because it exists only to emit the header this now sets by hand. + add_header Cache-Control "public, max-age=31536000, immutable" always; + } + + # ── Fiesta ─────────────────────────────────────────────────────────────── + # + # Mirrors the dev proxy exactly: `/fiesta/live/api/...` → `/live/api/...` on + # fiesta.nearle.app. The trailing slash on proxy_pass is what strips the + # prefix — without it the upstream receives `/fiesta/live/...` and 404s. + # + # Proxied rather than called directly from the browser so the API stays + # same-origin. That keeps CORS out of the picture and means the deployed + # console and a developer's machine take the same code path. + location /fiesta/ { + proxy_pass https://fiesta.nearle.app/; + proxy_ssl_server_name on; + proxy_set_header Host fiesta.nearle.app; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + } + + # ── Catalogue ingest ───────────────────────────────────────────────────── + # + # The API key is attached HERE, by nginx, from an environment variable set + # on the container. It never reaches the browser — which matters more than + # usual for this one: the key carries `require_admin` on that service, which + # is a superuser, so the same key also reaches /api/catalog/generate and + # /api/system/init. A key compiled into the JavaScript bundle is a key + # handed to every visitor. + # + # This also settles CORS. The owning team's allow-list does not include this + # console's origin and should not need to: the browser only ever talks to + # its own host, and this container makes the cross-origin call. + location /ingest/ { + # ── Where the ingest service is, and how we prove who we are ───────── + # + # Both are environment variables so the deployment can move between two + # arrangements without a rebuild: + # + # INGEST_UPSTREAM=https://mcp.nearle.ai.in INGEST_TOKEN= + # The public host. Needs a credential, because that host is on the + # internet and its guards do not care who is asking. + # + # INGEST_UPSTREAM=http://: INGEST_TOKEN= + # The internal Docker network. `app.nearledaily.com` and + # `mcp.nearle.ai.in` both resolve to 72.60.218.25 — the same host — + # so the two containers can talk without going out to the internet + # and back. No secret has to exist on this side at all, which is + # the whole point: a credential that is never issued cannot leak, + # expire, or be pasted into a chat window. + # + # The second needs the ingest service to trust its own machine. That is + # their change, not ours; this side is ready for either. + set $ingest_token "${INGEST_TOKEN}"; + + # No 503 guard for a missing token any more, deliberately. + # + # It existed because an unset token sent no header and the service + # answered with the same flat 401 it gives a wrong key. Two problems, + # one message. It cannot stay: on the internal network an empty token is + # the CORRECT configuration, and a guard that refuses the intended setup + # is worse than the ambiguity it was written to remove. The service's own + # 401 now names the fix — "Send a bearer token ... or an X-API-Key + # header" — which is the sentence the guard was standing in for. + # + # nginx omits a header whose value is empty, so the line below sends + # `X-API-Key` on the public host and nothing at all internally. One + # directive, both modes, no branching. + + # `${INGEST_UPSTREAM}` and not `$upstream_variable`, and the difference + # is not cosmetic. + # + # envsubst rewrites this line at container start, so nginx parses a + # literal address and behaves exactly as it did when the host was + # hardcoded. Putting an nginx VARIABLE in proxy_pass instead changes + # three things at once: nginx resolves the name per request rather than + # at startup, which requires a `resolver` directive; 127.0.0.11 (Docker's + # embedded DNS) exists only on a user-defined network, so on a default + # bridge every ingest call fails with "recv() failed ... while resolving"; + # and a variable proxy_pass stops stripping the location prefix, so the + # upstream starts receiving `/ingest/api/...` and 404s. Measured, not + # guessed — the first version of this did all three. + # + # The trailing slash is what strips `/ingest/`, so INGEST_UPSTREAM must + # NOT end in one. A name that cannot be resolved now fails at startup + # rather than per request, which is the better place to find out. + proxy_pass ${INGEST_UPSTREAM}/; + proxy_ssl_server_name on; + # Derived from the upstream rather than hardcoded, so it stays correct + # when the upstream becomes a container name. + proxy_set_header Host $proxy_host; + proxy_set_header X-API-Key $ingest_token; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_http_version 1.1; + + # Ingest submits return 202 immediately, but a sheet near the size limit + # takes a moment to upload and the service can be slow to accept it. + proxy_read_timeout 300s; + proxy_send_timeout 300s; + # Send the upload straight through rather than spooling it to disk + # first — nginx would otherwise buffer the whole workbook before the + # upstream saw a byte. + proxy_request_buffering off; + } + + # The `/hasura/` block that used to be here is gone. It belonged to the old + # console (daily_merchant_web) and nothing in this app has ever called it — + # it also carried a Hasura admin secret hardcoded in plain text, committed + # to the repository. That secret should be rotated. +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5bdc52a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5160 @@ +{ + "name": "nearle-console", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nearle-console", + "version": "0.1.0", + "dependencies": { + "@astryxdesign/core": "^0.4.5", + "@stylexjs/stylex": "^0.19.0", + "@tanstack/react-query": "^5.101.4", + "leaflet": "^1.9.4", + "lucide-react": "^1.33.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.2", + "recharts": "^3.10.1", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@astryxdesign/cli": "^0.4.5", + "@tailwindcss/vite": "^4.3.3", + "@types/jsdom": "^30.0.0", + "@types/leaflet": "^1.9.22", + "@types/node": "^26.2.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^5.0.4", + "jsdom": "^30.0.1", + "tailwindcss": "^4.3.3", + "tsx": "^4.20.3", + "typescript": "^7.0.2", + "vite": "^8.2.2" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", + "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.3.0", + "@csstools/css-color-parser": "^4.1.10", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", + "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@astryxdesign/cli": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@astryxdesign/cli/-/cli-0.4.5.tgz", + "integrity": "sha512-3kUYK5K5eO77O4trXmzG/GfwMBiFI2Y7MOfsVK3euHZk98zQX/Z04MXag+S89v5qeObS3S2LI27QitkIq7hoOQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "commander": "^12.1.0", + "jiti": "^2.7.0", + "jscodeshift": "^17.3.0", + "zod": "^4.4.3" + }, + "bin": { + "astryx": "clients/cli/bin/astryx.mjs", + "cli": "clients/cli/bin/astryx.mjs" + }, + "engines": { + "node": ">=22.13.0" + }, + "peerDependencies": { + "@astryxdesign/charts": "*", + "@astryxdesign/core": "*", + "@astryxdesign/lab": "*", + "@astryxdesign/theme-neutral": "*", + "gpt-tokenizer": "^3.4.0" + }, + "peerDependenciesMeta": { + "@astryxdesign/charts": { + "optional": true + }, + "@astryxdesign/core": { + "optional": true + }, + "@astryxdesign/lab": { + "optional": true + }, + "@astryxdesign/theme-neutral": { + "optional": true + } + } + }, + "node_modules/@astryxdesign/core": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@astryxdesign/core/-/core-0.4.5.tgz", + "integrity": "sha512-rbGaoAGZq1QImY2VWeWQNAh1ZqQa/KLWnmoOdy13lSjzMcAPDS6WrVv9fZfsNDx+DIG2oySLtMikwZk5WFl1Uw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "intl-messageformat": "^11.2.9" + }, + "peerDependencies": { + "@stylexjs/stylex": "^0.19.0", + "react": ">=19.0.0", + "react-dom": ">=19.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.29.7.tgz", + "integrity": "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.29.7.tgz", + "integrity": "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-flow": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-flow": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.29.7.tgz", + "integrity": "sha512-KYIRV0BuaN68CDdsqFkAD7MU7yipUqQNuNElwATdxaIdpTjhvtY82QvkBJs7zV3Evxj2jFAAZ1iO8nyy0nhjqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-flow-strip-types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.29.7.tgz", + "integrity": "sha512-AMGJoWuES861riy6pcB0fphE1YXybtQnBYQMuIyPv6mKLiosfa79BKTnAOyx215c/3RJPJpdQwoHZ3earVH7AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz", + "integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.12.tgz", + "integrity": "sha512-3vLQK+dXxhBMR2Wx99PTCifE+vHtW2ndZWyla8yK813ev6oGhyn8Lja8jCyGAWTJ+LEYZK7EVtJxrDj8ztevJw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.7.tgz", + "integrity": "sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==", + "license": "MIT" + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.17.tgz", + "integrity": "sha512-cN9jhVqT7u0K9tix43fhjoUwL0nazyW6zsNIXs2QdPADr+nurPfYyssUiMqcSCGlPcCiqnYVxgSn7zBSuI+5Bg==", + "license": "MIT", + "dependencies": { + "@formatjs/icu-skeleton-parser": "2.1.11" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.11.tgz", + "integrity": "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==", + "license": "MIT" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", + "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", + "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", + "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", + "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", + "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", + "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", + "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", + "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", + "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", + "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", + "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", + "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@stylexjs/stylex": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@stylexjs/stylex/-/stylex-0.19.0.tgz", + "integrity": "sha512-CnUFp7YMaDLDeemsWOfJgoC/gKM5P/yBNMcpJaE6ChJmXr7s0DJwSeGTTlHJcqqwN9OW1qGtmARWLFhGZN1pTA==", + "license": "MIT", + "dependencies": { + "css-mediaquery": "^0.1.2", + "invariant": "^2.2.4", + "styleq": "0.2.1" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", + "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz", + "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsdom": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-30.0.0.tgz", + "integrity": "sha512-uAHGxujGE0cDaKGdK28zgDotFtNA7MKq5DXl8LrfdxdCI8VHcg15oJz+amHTChPNI5JpgEPQWc2xFdrw3em/nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^8.0.0", + "undici-types": "^8.9.0" + } + }, + "node_modules/@types/jsdom/node_modules/undici-types": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.10.2.tgz", + "integrity": "sha512-7/+aSjzkUoLc92hV22bTW4aGanXf800zbwguhcICs0OAoCF9wDOE4wkopQ+SqfhXZm8mCK8gHpdTs7pZUWzK3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/leaflet": { + "version": "1.9.22", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.22.tgz", + "integrity": "sha512-h3lhECYEKDasG7LFHu+GiHqAvsgLuQvlJvVZzJDGONo3sEL+wUOqSFLnwkZlK0qVxnxbuGFW8iBlJNYs5wgndA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.15.tgz", + "integrity": "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "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/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/css-mediaquery": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", + "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==", + "license": "BSD" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.411", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.411.tgz", + "integrity": "sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-toolkit": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.51.0.tgz", + "integrity": "sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types", + "tests/browser-compat" + ] + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/flow-estree": { + "version": "0.328.0", + "resolved": "https://registry.npmjs.org/flow-estree/-/flow-estree-0.328.0.tgz", + "integrity": "sha512-uMB3dC4nfZYn+dd7/PYkyAK1mqBR4TJ4TWRdOjubpb/ObrLvPFdFSmMTq55fqttCCTygQ4NtD1Zdd+wcjC5t0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/flow-parser": { + "version": "0.328.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.328.0.tgz", + "integrity": "sha512-F+Ik2Of7ndl2FyPmZ77NrhbZp9lYjIsreDXeq4fo0sjxa3SIFv48QAszBqvPeMMzmw7JzqySJUxGR8CSD7EdXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flow-estree": "0.328.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/gpt-tokenizer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-3.4.0.tgz", + "integrity": "sha512-wxFLnhIXTDjYebd9A9pGl3e31ZpSypbpIJSOswbgop5jLte/AsZVDvjlbEuVFlsqZixVKqbcoNmRlFDf6pz/UQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/immer": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz", + "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/intl-messageformat": { + "version": "11.2.14", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.14.tgz", + "integrity": "sha512-9f2VD1HFuxUvMw0RxsaP8WmMns6JRTnsNB/zghTFrp11ZktiXWwVDeZBPQchBKEmo+Gx/ZhxI7Qht7YglFD4PA==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/fast-memoize": "3.1.7", + "@formatjs/icu-messageformat-parser": "3.5.17" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jscodeshift": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-17.4.0.tgz", + "integrity": "sha512-i3ESKiiTsGynxzTg5BhsZViD0ai72/6SsI1efDZxG6/5KCoElsmquxtyhXK5lpEgoO7MTNpYkjFEdEL97SkBNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/preset-flow": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", + "@babel/register": "^7.24.6", + "flow-parser": "0.*", + "graceful-fs": "^4.2.4", + "neo-async": "^2.5.0", + "picocolors": "^1.0.1", + "picomatch": "^4.0.2", + "recast": "^0.23.11", + "tmp": "^0.2.3", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "jscodeshift": "bin/jscodeshift.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + }, + "peerDependenciesMeta": { + "@babel/preset-env": { + "optional": true + } + } + }, + "node_modules/jsdom": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", + "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@exodus/bytes": "^1.15.1", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.2", + "undici": "^8.9.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.33.0.tgz", + "integrity": "sha512-MTRwMy0ZlL8Ur/vOAiJ9XGHE+kFPC7brq6MxAm0GiGXEBj0qy0jA/pG4N675oSzciO/UCdX8T+5yUQdmDeTLxg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/recast": { + "version": "0.23.21", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.21.tgz", + "integrity": "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recharts": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz", + "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", + "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.146.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.5", + "@rolldown/binding-android-arm64": "1.2.5", + "@rolldown/binding-darwin-arm64": "1.2.5", + "@rolldown/binding-darwin-x64": "1.2.5", + "@rolldown/binding-freebsd-x64": "1.2.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", + "@rolldown/binding-linux-arm64-gnu": "1.2.5", + "@rolldown/binding-linux-arm64-musl": "1.2.5", + "@rolldown/binding-linux-ppc64-gnu": "1.2.5", + "@rolldown/binding-linux-s390x-gnu": "1.2.5", + "@rolldown/binding-linux-x64-gnu": "1.2.5", + "@rolldown/binding-linux-x64-musl": "1.2.5", + "@rolldown/binding-openharmony-arm64": "1.2.5", + "@rolldown/binding-win32-arm64-msvc": "1.2.5", + "@rolldown/binding-win32-x64-msvc": "1.2.5" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/styleq": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.2.1.tgz", + "integrity": "sha512-L0TR0NQb+X4/ktDEKmjWyp27gla+LUYi/by5k5SjKXf6/pvZP7wbwEB5J+tqxdFVPgzbsuz+d4RTScO/QZquBw==", + "license": "MIT" + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tldts": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.12.tgz", + "integrity": "sha512-WylhSDKVeYnWXL3a+vKTaOxjnOeEGw938hImY8zoRWJjRRK/Jp1K+IihBzIONpUmW4e3WmXT6q5FW6vlESVZCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.12" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.12.tgz", + "integrity": "sha512-nYNzS2WRf4QJmjzFFgAxLOBjyBxAGRbCy9PVBPaglcYyYajh40VBn+v5Ngr96ZMc7oM0+aCJdtQnNejvdBnXMQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz", + "integrity": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "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", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.1.tgz", + "integrity": "sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fd41482 --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "nearle-platform", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite --port 3100 --host 0.0.0.0", + "build": "tsc --noEmit && vite build", + "preview": "vite preview --port 3100", + "typecheck": "tsc --noEmit", + "test": "tsx --import ./tools/stub-css.mjs --test \"src/**/*.test.ts\" \"src/**/*.test.tsx\"", + "contract": "node scripts/contract.mjs", + "db": "node scripts/db.mjs", + "appgap": "node scripts/appgap.mjs", + "check:health": "tsx scripts/checkHealthPanel.ts", + "check:optimiser": "tsx scripts/checkOptimiser.ts", + "verify:live": "test ! -d src/demo && test $(grep -rl 'await fetch(' src | wc -l) -eq 1 && ! grep -rlq 'src/demo' src/ && echo \"clean: no fixture layer, one fetch, every screen reads the API\"", + "appsweep": "node scripts/appsweep.mjs", + "appfix": "node scripts/appfix.mjs", + "preview:map": "node scripts/mapPreview.mjs" + }, + "dependencies": { + "@astryxdesign/core": "^0.4.5", + "@stylexjs/stylex": "^0.19.0", + "@tanstack/react-query": "^5.101.4", + "leaflet": "^1.9.4", + "lucide-react": "^1.33.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.2", + "recharts": "^3.10.1", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@astryxdesign/cli": "^0.4.5", + "@tailwindcss/vite": "^4.3.3", + "@types/jsdom": "^30.0.0", + "@types/leaflet": "^1.9.22", + "@types/node": "^26.2.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^5.0.4", + "jsdom": "^30.0.1", + "tailwindcss": "^4.3.3", + "tsx": "^4.20.3", + "typescript": "^7.0.2", + "vite": "^8.2.2" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0239e38dab08b195a081c095bcd6ee26bc4934d0 GIT binary patch literal 11108 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 Ht6l#GVNE)- literal 0 HcmV?d00001 diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..5060a965b0867a8be08be697f20a9b9b13eb1181 GIT binary patch literal 2008 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 E0JtmymH+?% literal 0 HcmV?d00001 diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b04d5fa2c94d0cf403ac9bb3bf7f554f5764a50f GIT binary patch literal 18637 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 F{R3r!maG5( literal 0 HcmV?d00001 diff --git a/public/icon-32.png b/public/icon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..5060a965b0867a8be08be697f20a9b9b13eb1181 GIT binary patch literal 2008 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 literal 0 HcmV?d00001 diff --git a/public/logo-512.png b/public/logo-512.png new file mode 100644 index 0000000000000000000000000000000000000000..bd0d622194232a3b16860625c2be6104f50657cf GIT binary patch literal 31096 zcmeFY_^Rq@`nq?hphF+CT~El5Q0wq&ua%bGSF( z-}C$j_sjbQpJC?gv-jC+ueh$Y)``;5QX#^p!-pV-7I`A3@M&XuBvK#hChaJ~j7;F3 zN~vUx(q# zF-C8~{PB&?7oniz3y?yt7I54x%+LbTi-7iVww|#Z~ zVWElH*$@i{;=zyd8srA{)Zk2Tyy>kC_Y=BKbfyPwMU@R*8Mc^4ytcKa6P~cXmy6%J zaF50xE8F$>V;gen&_owG7$UL{jO!n0Af9fZcex(nYg?@oskh9jt1ejH;p)nBHKE2Z zQvPmjDUftgWZ5DzaS5`@Bv3`!F~GiOWH9SUWFYId73Gjtj?6z z86~G={bK8CeW`ECR2%TROeZM`GRurl}F~&j?15CV-uO!oPny9}2!WgD>sv%PnAhf5;yU`cfYk z>;7)mTVO0Bb4Fe4wWw4O{bCk&1A{ypMgjCNm(2ANEp1YAaB@MX=c!U}b6}xc_($W< z!tY&=*vy95f$}sAGg{X9a3&~8{K=%ys^3dtCf_pV>8x4AM`r6#QX0PtAqA|^H$YzG zusE68n|0FZKGLu#M!-?+x8`SNPyR$aKUBs0Xve?U`b#hz?_n-5rBdsIIpxm7C{D%R zW}LiE7n@UG@R*-+bf84zexMJT3L&erbyz_1!Dhr!EW-Nb;sHwmrnH&OJwqAUX$p}$ zVC2Jc9~buHfCJ0aO)~9T7q?h57`Y!e^l-`EvD>0+=VTrkP?k^^u)uqWpD3;XL$KZG z*RhoJ&oP^#GMl}c_~gWM9zDbN3fV|d41S=vJ4%R0GbM$CTw+uL6e_{=9VqnEQW|(L(g%RlBCYAu1 z(Dedk5kJl78knA>I@Dw9sQuU`?!I)@j57`VSKidejDg~-Hj3c{TwP;2k)d#WXo21e(f2U|d#tt(#+=U3hV)%4YJQ3~ zT;1MkEiY!y7Hh_~i?2^`SBYTXJ4^Bg59DvTf|dcxxY7kiq|5Bf^fLJHuiA_S*TRl1 z10g(eFz1~c`t6kSJ{*-o?PtCW!_swUxvT0rAb~K ztda6(q}Ga??>x=REP`d@x?mJwMb3gJQ`wZbADPou_jYOJKS`9X%pNWo19xK8Hy9GH z^p3esI~rQ)r$<}@4>6VtbTPsVjh}oeW#x9MUw=N#{KK;ithkHAFSY#`yKyu;udbSa zAHAe4C1FUOBZfMKVL_#YURN_h!X7xqfkzSFKU_*xG?5_Z^nd*Wswuwrw@*q?N`vqf zP6Mpn0hrWNVEf+7M;KE_em&-a|70!P{3rGn=?#2J1=6~rb{hUg&09ug`>hhOn1Nlt zC>2n2nT`?_+t`&m^>}w3n&cR6OWn}EU8GbIV-E@ih0joB223o+Pgg&tqZk!|l zU2qvv`fIM@lpzz!wK1K0S4cvpsPDE*Nvd)vKuWT$Om8ThL$g)>P3t^v5MV@Li1QuJh-k%3HP#CW$z>WOJQ!3;}4(aJZD>!Q2&|0=%7^62e=_xe9o7 zCWaa;Zdj=q?+c=C9P59Hlmzy8a|H3svce+WI;gJ5#AYqSCy5VqAda^I5nl6aX<6Cb z{dRo7bi(v|e^(we&=EG+*#bw)aN4^V&y3t26Sys%DVO-_$)vFO;XcJ?E#iedED>>Y zw%a>dt)%?Kjf=N8n%Jv*Q!E`h-s`w#hI(Fp&hn|DV*{Rmpszj<#R}6S!$*a(E?jM{ zkJ9uqUiIsaAVI9_yIQ@oL%qa~W9H3p;ygXKS_)#Lq*fwF<=Sjrhezw}C@!!ws~fL> zLI3kc5R#$`tECFFzT!z)gTk|3Ga~!#>du0!{alJ>kct2as6DJ)n*8q*z&nF9ui#gS00wz?JyG)xGG*?8>Jf)>`TzwruDjbV0lmt#Ma^4>;oG zvCjdk|LnDUCf)5xKj5T?y`7N|mFGwGxv)@>=|JsoM?~*Yy!{mz*Y$j1wy~{_ZS9s~~%pnlTk%qza0YV-UNvYe+yKS3#nci5$wTdt+cQLW4+u!bG?B*=uhJqi{)Eso`oWZE8<*Om zA99A`x=eHhDN^90-u6$|wH#U8T8Jkx-9#_mogi*lD9oR4RkhnnjuHk6d`YW>$jyc# zVI*5SZ85-x;*cm15?r5^JaF5vs=J^DiWQ%vTO4b;QZt7=IFZ@nHG7H8Q^sxtHsZLHe)V&vCi1uv-R;rR2yGA#R9`Fb?VMLVmIHvq)Xz zovjMr1~>KG@1GvGF|aX}5SL)f-1pJ*Ezg&)`{(VCFTA1$qoGtxY#b#-J-EgKuC=ah zU+08wqOr7i;WMt{I#J#jN(g^&k>=*&pKaMF@imVUfiycIi)J0ncCdL|xkY9fAj1ph z$@R7ut$pAW;`0q;9<019A>Sj8MxDyRNXdTqiS?gG#CErhGTNc5t17*lvT^&tHhZs{ ztS?-dGa0OfEO5QgQ@F;sSd?`jMQC6tB9tJ{<68CCWwG+L*~JXWJwk!`E2~JyvAf(d zI#6gj2yaps=P=PTJlY%hIR7R9R^n5_rTgwWWlXAOV8G91d$r^whPCQB@LPT2k}S8t zT{OhUc>HN}`K;-O!{p^1{K7FptspGK!<;=5F(c(oyU$$>D@`|G z#o|Jcfs~R@TuI;@Op!QJMVJ|I1;r^nbbWH(HH~~dava;%i=vp}4;TN!o@Ns%)Os@v zgI_9sd**v~nq2-I$E`op*Dul!F=-_I!YomVAkv3{>r972kr>>Nd?(O)@+?j!Tc=oL zG<}*(<(2q|c5&yg*|SztrsOP*ixRqp@QjNKhHBs2EkK-X$BZ*6W5lkyEx)#bt04Y> zvsG8O7(XZgeSn8019K@^S!)3czeK=D$pN+jJCY!9_leD@boM{3HSE%*nRCQPmAC=d z$Qbp2oRAV?y+KpjO|5)LOS{cY8xT(&Wt|ATt_{|ZJ&h2+_a1LTRq5JWy-OVkvj_OihZtlq+b`SV`B!%P z<^kYgs;=JZ0)c}&V(iRhNe_h{GIB%kX8@qZTV;RR6JNXRT9S1Te@E2(9W`$Uyp``DJA+t*;n{Km z*tev?kn)N_THvyQqG`!6lQZ<-;8IgUM5sT3xlt4+NU$#~ltTIvZ0fWf{Y;tsF1SuY z3CWbZBbgV`WF5bjn*M}H&+v1wivPlxGey~#t@N`*V245Y5cAs4qBa?x>4!HkLyT{H z_G#H#lw|rLMvb5rKZ4Q+1T#V`OhPPF0wJUAFY`yV0T)SIT_s1wem7%!q)-q4PGrv4 zqfj}}ZWPeKF+9FjQ-Z4;D-$`lUjfGLk>!aNnsRuNGrYZ=>(Z@ zLZHe$O?(a$AQPAnpdQ+)9Zn*=LgI` zt{WBs7nn?_73@uI*3GMw4#D8}7hJXBz{iIYz;iMAFWDbD-^?zc)jpImZ+==r{?{I8 zq-97pwmfVO27u%bv~?@;N=K0zE+dx_owFqAkKm@ZvZ%3=mHzCHYl-blln|fK z@ll8ze~@=*^5%7;bo%8AjusE>mZPup!x~HZ>}wsU#sZ+}S0{R(JA>YRlZ-e0spUJG zd@ha$Dsw-$msy7tD zoCCT11SG|KFM*6%Jo5Tv=kvuZ{}0APxba()eM?S`hu|c<4@o64&z1E12_o4TYdvTl zT)OmkvoI|@J9&80gsiBl9t(_3ChhH6DnyS-Dr4b>J*^o#ID`PW>G+QUl1p+!42VT$ z{`5E39!n!Reg#)gc}hE_iKiQl!iyI(j|l;tE)}m14XahU0qDXKF&$@|0WSTFcYFWw z1}+1LkPG3`<|@pcWJ!}9>RI7np)^4}9NXi!1+Gf=Dq;HCUpWbI(b9W8W><^M4+7Qx zOxL&VEk+?DiG#FwJRW**LoZv)noj4RB{6tK9lw2--qCtVe(Q*_#>b8rm%9G+>LrWA z=42gKk>jxf$tS^0$;%SnAQeLem<2}aRYyWtZT-imSkll4pss`}SxDhko1w5=z9}H; zhy19n68vWDyB9Icy|LHI+mwza0@NBInK;Z_9a-ot*2}e7nYd}34Z9>G3XiU)lUrt2 z>m=hZ#4#EC=l8PHNO`%TyJg8X$kKMztEOb~N)BtW_#G?~nTeR$i)FKCl9P#BFT2=r zaQe&d86BaA6{mr(-(g@}{w~b%>cq2DA~8Y-G7hb~_IB%#yFSGkM~e-;a(t4&AFxGG z;IuPt_7r+V;i0v!5ZSc*WRtN6Rswll&(E(`BlV8FPAX=t>jE!3IWMSB=@$o;HQiLc zkCkVJ1O?|g4Ep+S@gK97dmhcUZ2mdRPnguR!EQ*#CtKhLoC>g8_xtrRZ{%SXFrHo) z5b3+Z^shNrZRXlfX7ar+f~c9gGV{hO3)7WFTzqy1%>36J%)i=S^2eEWte)-VClrbL z|J;PVeOKQWhj=*VT}4DpCeMq0CvZP|&x*f5Vvaul9xaH3-Zn@?FgLkj zpVbYoU{0BTu1M>Yo!Xt)WWq<$xtuWbwQj?p`yXAm$ACiPQss^(i~@1*^%NjO85L5z zg3HBLc004O3TZEJQ1RO@J0`n%6@$OpvJB@?H%n7OBH68k)G@H(k4!hnj>f4+e zO|Ub&QULNd-#Zx`by~dWyHa?%NCHip@cKc&d739s{?escWmNrx}{=3j?ktj;!2QBZU3ZSRq3 zn5MUWahzVAoi-Uf&61UgMLuvFDci$Ajo>Ha@Efg?kcE*xA|LNsD~!*<$%X?W>o(+o zi|0pfA#ua#aZa2xFE4QmQpm=8JvUFMWlZU6qrDOmSjwxguIj3Dz2>T4i0+Z4KX9wE z{bnxmL$>MMG>ak2%!6J5nhzC*rz?;kusmgcwI>OXcQB68lhFh(ioL6J;7MJ1ccUen zH0y1juXpkXdRDG4FP!JP&1VfpPtCz1P3XlY3@ckd+e@nrCtL-p&Xy3lbGcg_>R zu0t>4BNLh9%|8<0d2dlr=RwfNm9A3juOxLO#0!_M4kV^I<( zMG#~!T*gA-*|DrVzxo%Bm$t_<6^#1M^XmpQ90S5)1j7d+rua2j-9ac`#NF%>}aqaW0vhLDh z=6HCzddZdg`)8rcA<(O7v8LBw1zW?E#s?RNg)VJFxw7p{dUuH*wMli$0kbVWt?pz% zx!hm`kdw)}qI=oQ08xCv@gw&LiYM2yEwvk|no~EwhG)Zfn3zi|%Cmq`ffJwmc(4ex zXoLHUY1F%KkJEkbJvM-8zN^@>tkpSpFA$G z%a=5%V=8_+90=(UYAd8z{HS)&j@ASK48*jfVDA>*gPyzI}f1UBJ)1 z(U#jHAk4k-No5oTKjrhgAmv!f(T{xyt(F|}PhI|A=rC&Eot2hZ7VabDTga_vSt}7$9eVI8~As z7&yw4kp>!*+cV-rU~YWlBWP1bVD8%*dkrxAQq%VoLXm|Wi zZGJ73)jeIeUiIW&-p8>Rw&x&t8JE_vKPJM0Dwbr)oeG~FAMb2;oKdu8?yzCywilXN zy`zekxwd7a>wZH}{nD<^IZInaMoKswt-@<`GDg&)SVV85=X2O^(XSqfNeNl(n^S%r z#bB8npGnO4SI4ECPe&oiAwt|F|Ny^SftZpAE7eJJenEB}za82o8*-2@W$z z;EbQ?s}B7wqsIIGe<=7N(rX>m@RBCit5I(Twr-w8qux-}nc2kBCWQ&Tl!8*iBLod7X7l;=hy8cG~p4 zK2m{SR=4zH*1NT+$<_7R;-xCzgm0KDFvSP}cHa^jM<^1YfN26mOZKAHtWFe*)ad)1 zo~If(UafHu+=%^G_L2xL{+D^so<|oAck%Vjq$^aF4`IwLO%>l~M};Pj9BVA7xQ?<9 zz){=R_N1Nd1?p|1B_91BL|YL43b0DG_08N$-y;8B&!w`{a%Sm&_D0S)b&PjZOG>y9 zj^w-$0Bf;iN}*3GN8eT6BjV@9;Mw7xZq`8rtaENwffU#3zhYyVObQYx8TzQ?wSyfF zdy!}i>sMK1rhPGN=5^$qY%nwNg@}9{B&Xr@lifN9M;W^o3umS&dx?uWJ^HyP)dbw| zTm^v00}}S0n8&sE!o4H6uYc-vW89#W_6}8}E~Gt@89f*Fw`RX*=JS2nIQZm0vWp=hws*6}A(zEz zjLdcaQO?VD9x+S61$WR(^dc19mn=>>sQr3uv0urCUNcuzp8hHC6+K=_c2l2raA{!1 zk`rP);(Gm`Aa!|P-yNI?z6?IdnAVBo$Qd~%%sfK}g_}#=WB@@9l_;QBSuo(JN|3Cd z&nO&exfYmWbuoT1A?WAueW}WCV#l?D*cZyhYJ-MVKiz0~#n*o@7nArCS!CleaT9x5 z2S7Vs{{DFkG0zQ}4@2EyAbGwau9B;V)B1un%(oW1nx5en+qUJ;Y9UasI2=-g_gwqZ zf(mt!tEfVa+O+Or~4?hk7Bm=_K`PsZyXce@ph-AeZHz z7p3mfNN-!h1O^5dk62Lht9O4%v%^4dMs=|RkkpZIel^+GGy%^Lg&;Onot*lTBrq?_ z;`Qsc-@7SMG5TW<=KRHchJs_JH}8C1Hat!SgyOSDc-+Jpy*mJ@2{>MoeK=p0tI8Q6 zo^5o{dEKS21l*EAM`^u0j0r_f&JK-T@xM22=>M^X<6^V)K#Td?On|fPy4J7lviBzA z&fjBA6HK|Y%}#9(FU3bZs(g6hWf@yzf&$iqM?`djnJ91fUUuCI!LNkSYoaIhG!I09h5{d32b z8^rs4bZ{U8OF7>tvRyNjGwNOly`U#w;FU;vA`Ps0zscSz4{iZ=-FR5ZZ*e~1*mVHX zNVcx?LoYjKw`9a454eK8T4HqmR~Asy+}uv$;?uG`9+9ddmO=SF5&jP>g(CWn({EqO zWR~4UrxU};dxloER9lHCU@;u>H$LfZr9bC<+qTxmGzI(Ve$y3Pi*e60t_u!^16UaROzd7dPP*!1u*8vmDbH7iDx?wD&UwwTz&>{ z)!*IWAAHwt>qPMUlSUy-(x=;_lkkhjW(-v1uQ8fWk*`W8Jw<>gSV>5z;o}z`1UB%} zPEtmSBJ=p``^-*>^OB} z*;sxEkRaG;;V5R$-2T`u&dY&=rj2|}f$WZyI^U7SHsOymcY76D&8T!7B9j?!fDP~g zY0BTN01~;y<1!yr`ggTUPncz!g`Gg5l`e`Dx-9t^Gx)3k1ke8gQq>cq)-SmwcV z4g>_Kph5kN<)4{!f=bC*BFjSjl8N?j14DF}93?{c?-mv(7%v9mqfS?#k`wn=dpuEh z@!A`Bn$-)nFpIoo>!^4*Oa(|-VDN1Jc*!W@*wP91O#_%^%M09Wv=oFt#H}z%>J9^0 z?9#$x)zhycs_~C*YZ$<)71r6ReKo%t;oXquR{xr0MOBXnZsYAF*wK}c1>B+nuKczTnbvG+4=`b8odJB39Q97=|T(AK$D1Cs_tR~NS@F+Jf#jQ`A z!U5(Y%L}m)@qh1?*!yepmVu;7SJN0PeV7!{@VW(>gsEhF;(~WRfjFK{D>2@N{aB1! znI$|tYolSV9>i!?YhyWvM~bRyW5yOi7C6nT6eEn!9=&DA>Re;r!D@)rvCPwk`@!0? zZYD8=1YiQ^p`0J2f2T|A2U_Hy!X)P@Tns&rf=7jbB+<^mevXZ=UCmyVZg{ZJ}OYyp?&jVI{vzUcfwUIXF-JXAE z)pV}gv+qrvU0-5B^pFz0A>`d%Yu#2Ol)l-xqV;u@h%}p;WgVcu5Hop;c0;HHj!M+K zL9hzK2aAdA&pV_q36^1s?>UU%|6OI72op(vTkRBVO@H6gaCd2QXl+XazH&OOXfH05?6d)l&T)*3qoi-n> z90p$?g_n>s6aQFbbJg|=RcfGLxC60me&M9E+=7+Jq;=f%d>kAC(Cn)5Q6A>Xy;xBu zZ2FeK{A8r>}3lC&V&t`)wx$YV*JS zajY`9v$~yb+DNbgvL`?|-1h(o53`=m>;4ZOmgxWdR`fsPIeTlHU44eA&(Op52wJXI zI=Fycm@uWz#h7c$@9o6TsDWj7yh_?qN~l#+Jy)HE=pkp{-<#!_ZnpyLSV)5-iV-cU zx(>2bG+WnPLX%QWQNC@l;mo3LC>iaRf%XvKQ_RJ&|kyO+pG0ULsC{GKryJY|}l5 z257$&xwD`}E|ySA0TIF!v-*#ruLsqYAj_=)dtgcBs#ULUNQ9vd`>08t2k+E^rmQW! zaqz?}w^;0KxV(46x~|M46TGSLBNgZ^f`(58)M2Vakr|K$MfNI%;i6>+E=agpuI{!G z8OI?xSSc;2dCgGtX&@L9DnNoLRur%kLbn2iFQ67gx)u{f4YqsR?ZM}ZgCq4A>Xlpc zCFBd#x>^e!YMYn{y$y*)MdQUT!v=L%dkTM0@Kx?J^SnGQ33pAmQtE$7i&nakC0o>o zuK$1Iv8+%X(n!~tf)MJW!51j`YbI6Lnv(dUT^s7hAoXcA*57LX%zP9T^#4K0U^8Xz z)$Jl?kj(9lC<_xQRVH770dXg;QYfF4CZow+aj`(>&?Yw}sWRD0_=u;&u#(sqS)yX( z7#>6nj~2C@mXjd5=Lqu20JBS(XDr%<_NJCdYwyXh8ya0u!N?@a8zb z-DZLWl?V4bbWq_`{G<`^*W6^1pf`)T;)lHeRfK5E|8SdS_lbE8MWaBx$&YHMuYSzL zSZun%poJ#dJZvazy{hHW&?F{C^E>t(llZMJLH3#G7;&ds5{?=0;3X6m6}60dFpJNG zg-p6xG&R6+>XPB>gglMWpIQ={>Qh4*Co4|=Z&h(MJ_Ge8nG;!?z12YsrsBd<~)>2 z_vlmx`=SfwUsGK=_XW1--IeG8o=(Q;dO`Rn7M7Y^P+6}09c~>0e9Ffc_!}#$;TL{I z0Zw9kfjV7L%vt+^*m{B0a95-3-xFziIytxQTXPrNn@6KLi+}2*QvK}4 zKfWHH$u^JYy|r>6ew;Cwcz}kqn zy^86jEmsN7f?09#eQ5WStTy8b)F+JG-6@W*cM`C!{+gDaZg8f1|FcUx5PJ5Mkm_)h z)~N6>#DWuw!Uwb75A_JYY&fZ;mz+$gk8QPrg(+F?9!~DRS3$IQ{Nq#-KK?O^5M05a ztSD*eoVmsW6o>w>+r6Xm`a#t7%8<&rgI;BOGZjc>8`!w^K2^8o{fvU_;Qy0`0?qU_ zRN@ryq12s!>7UiYq@Lh_HXnDQ4V9Z*n4ZqjOYf~Zo)5gRM_%`W!11vnOeq#Y^> z1KVmG-l3y2&zxyIk}Y+q-n_vx6uu9b{aAoYNl&`QL=*x{=Vq!gm;fKqV z!5V^?@1BVH46J+G^*s*E*z<6;EiU})!THsa-i+4=L zh3O83)^mU6-}?SI=_pBQzU8$CYxbM_;yl-}CX>RcYA^{37GI75&;MKp`3MvAt`c3h znk7{)w*Nnt0MPV4LAMEURxdU``M4K)!&>Qj+1d$# zf!%5d$;=NG!%S@GyLM}MY3C;;4=ZknyPVz)hbWbX)o)dP>b23WWo<`2+&2B2FSbNw zuoQsoD$f&3Dd~jLb&(>(!%Yq*OYmCFdn!1zYIp&EpR1Fbe^Ki0_$4uSu{%ndSq? zkqKBu$e`6e7XI${5r|hl_vs|5bc_>AbMo5$4qm=+6FnRxQ!R_ifN(dY&N#B;!VKav zWGE|*dGb#BsE-bE`2$Pa%wSFD%<~5`j}D-r$*oaejfSpmn)Px9PZp+}W)9qJKimpd zEoFVKY03U7r=rgaGc;KvM%8J;@83|N427#q#squ7whGpw$e0TMPQKbf?VS|mIt&J+W;!n{n#-I0%~Of~ zq8q(lL&3U7vJAD~KnNlgk=lzRf{%B<{JVGO33-YjXR-M0s`l05RaN8jMOlKiLvcwa z)RXU5PwM`>kaBZj*>8YZh0Wj~3?4n(DZfqV>@QXHk2LZzS?-yRoOUEtTm&~JoU7+v z;T0ROw#sUB&U^I}%Hf}%Zdtqi2{$kJHsW>OtycPFV#q@2)kp|FcwmzAe+C>5EyKKD zZTbtgL>*oO-p18bIl6z4+&KVpo}Vu?&Co-*9<(dr-}ZD$Qz}}ef>5ef2VcHqQzKdv zQvWzdTk`aK@?q)QHld&1qern5*5~^XwRdPHaUg_f<#y^_Wa({{Wx+oxDg!l$Z6|8?b@q>(iZT9b>&jwQo9q$@QM&=_Ln0dLo~%DxZJn^K@!z zq8dyO|GY{gID(ARDkzN9ko8QHP#XHB?;2>id8$Nf-twq1%}B!ujqiFzrNsPJKi2QP zm*~IJ5x@sw^{wa6o{WOxaEcBp!#{|NM0O<6vt5<7sbuN*DMkqc{Pf?jNsXT!8J2XA zi_Nz&SC@{%&py8^Olf>N?wdxdh7hqwomuZuKrbFZ_bGC%#=FKR#^ zQ}DB+(5_&+=W8gmNw>Ry@%SFr-KPrlei+^l(WAY#RBvU=K{2iS^ZP8#*`WNiw%@$? z_`LXrsYmX4QYyELxjOi|I}cTF6N+LR!hl}nix&&f2M#bOy;7styK0pW6JL&&p))ef zH!EDH?GZ49poy@V`eR>g)1*()Ctlt0Ijhxp#Jopn(SobSg03etb7`YaX}*K__;09& zxnHNGEgAU0dDT0j!djEROi&KDGpQajb!kC>$dn2&Ygv;U^3VqQD5+2J-acG2S3low ze`U!tzGYM+ZV+9aYW}E=8*1O4jVCL9q-+?^c+zy%sLeD3nh8mUF6#5IW?lsT%%iB* z#0k+=V&AtVB>7-=N5^<5)N`Qii#E^QuS&1~Jd^9Qq(W#IrxUE`yzAHP+Ks6}tIa$+ z)f80g$aVOuhJ9(r3d+HBE0r;4m7^3G%NMqilc4(5mnOo=$R*8CXb3mG@OC?jTNWMv z!tg=fM@iGHhqb>?PP*qdkNsQbIU=#nWQl6lR5;29@*tHY{cL|qU=u#;598@PF>X z8``aN3{E!3&BAHcv*FSIBAiq!t;zWnz>z&0xe&rMkA?jvu59l_7TgyD%r)P zq;Ndym-}^w>x*fcID{4iq$C|jhw&qSEzR$b^yuL6-3@96pI?;7!tzCz#p6^CJatP# zm~r~~Z2>Oa2XR*>szUzz0+X0^IZ0!W42C^}^9)FI(c9!-il@ILRrKvN?>*NM48u8; z3c`9V@O9Myqf9r@f%8(4=b#{TBGwom62-_zsMByg$hKPzcueXqJYKmYV@ zr~y=IRiC1K@!;2Pv2v01;Y;fe7P2K5XXD>6Ndp+}GET0Ff30LmwMiy59^u8=cl#!0 zp~Qh792HFbaQ(|`ILr1bb+T3K>--K{O>A>hjnH``qBat9tJr_kEdsY8_%Ig?USJ+# zB*|aU<+C&gDZpTsw~!;eFeeFLK0#A;TrWq@aC`~#2j7j&cqEaJMeSoafL01pK{17J z?zP6ebxvPpiY%qaJDT6|U^d@|c6o2N?R<)8c-lINfeTv-*ff-znEvfJyAg^a0vAuGis1;}c&c#E>)ICmkp z_cAtnzU=qFg3vNJ^jQ_9mBcWO&&k{6zrSmyMOYEFo+DC02ZvB=cmz=>1LG92k1IIb zWtlxDLQ(qE7v1@W)B0bh;y+NT=KqQO3_;|lLGL}UmPZ#Z%$c8ZvU%d5g1gE88BXCD@+p?q_yULXmUw}X|H68PYT zx0EPYACv5T(|MFin3_Dq1R-Lv5J@tR*hkDsso|COm)$srKY!;>BZ{i3DxIqJw+*{% z!UQqrRJx?g37QOlr9RcIv}T^foNVcCss>JK%0U#+*pxE zvB#{AC#=Fn*K6W$HQhfO`y(k#+f1oT8U@&`Z=6uNcZH}iA;u8>+RM@1VtE*)5-qQs6si%FVLT-FS9yb*QV=Lus%>UinZurY_p7pU$#meV z7R4eDnHmWS%X(cA8vYQavvFFP$v-(BK8^gTgEGi(*wKnD9u`2LxAp$G7>TqvZb<3p z;K3dT>Gew^J1gZ{)EcwB$c7q%@IT-@Wol$PB!{zC!7YS(S)qZUyzED>&bod3T*x-4 zIfE))ta`T0#Pg+9Xt0{4zuZ9`$+HeSm6vav1T!iGtge@iWXY_Jky-tFGduc%6M{_Z zjHL;6?kt;cKQA{;Er3TC;=z2BhTSkyqp1*KocaeWv{7258vB^zWB4fJRF6nPWMy#o z>`;%2ndmpan2$b1@zoxcmps(dlGXAw@UecgYGKm`a@ArlL&MIIz_X1yseoQ_z30^1 z%jIPcD4?&?B-u$uf1;_sj!YP>VE^o(#qg1q>zA5k4{G-wuKj8f{4XYbxxXM*{t9uk(wObcsG(OlnuZH>Yzr2^W>1ADbt z%!9i7f$n0JPue{_ye z!jEY9yLglLV>7KVf?$(0Pa1Uy#!~JX_xZx-Ds#-ozsOra0XF=u@~ykoivRk=GwHK3 zGYJdnP0@@eN8SO#mKxY15tyO4_RZmJgr=%4SP;m|lCHgjZdW%6p!dsOK0Dy){=uuQ zd$?^jqTVU>^b5(BQyZeHlY7oB{nc(km{Ha=$Wo8dXOkPaS-+6^)N7~b@amOBB^_L% znt`lX%walaym zNiAwG?chl{bFI{PUrUsIZOmJYbS5t5Rwl?w-0E$t_r3Lo6HqS!PG7DlleThMcx9_bOy zb&%>-SSKbV zG*;`QZ&!09FY1#QMA>czL1b@_pM7;wGMdicJ{GGy7NJG*PyC)}>Fbx136+5E-SUHK z_5gzZTckAl<;Z(!4z;lohY_E*HQc<-@?}2|@+?sGm%P@D3W}cbG&M7Ma@5?KEi z6npIeYFegvu58)dr32v?_D*7LTjn}(PfW}AiiZtHektY5Sl1IWzL7PeDS!$+VXuBT zI4F*Nrjv{~AHPk5=6E^r9FNf69k!W@A-Z`fmV*16*Ov*E8S{xUS{Ngj2QJ@q=OyVc zQi%1cL`k%XjoRAjNgMP03w^_*qiP}2PM+l;*+89QO1@ZO2x2*)zn9AYsqzc=NZTLW zCwU3og^n%B5y0c|&p*%O6o6t$sFPtuu^CRQXT|WZEbuqMRvM(SFF59!!o2mz9+I;{ z@*i-r^HGKJDO8R$U1e*S`cr9B;^X6fstt>Nx(Qo2ppO5vNz9PR*$a_uv@urEKrcCV zdyIPX6(*)y9>xT%q$3u8FVY}PR8z;WQnZCPdLY>J^~Z6J$h~VeP~SqFn#$yUG1MQ; zoln>{OEzf6@OWAr`iswO1%4O(Af3yS^68SNY{Yar4Q}(rre9qY%Cj4`H8*_b%z^dW zSEM=m4kZikYhI#fmrW0?FcEL}A+-f`S>{m_n*$y)>^%R+Oq6Mn;dqo^$*buC>zT^< z$JzpS!%E=IS_BrdE<9zMz7*SMaT>V_Ft*D_yrGL6gwLvn%IX>V@#WI5pL`~-2yLeN zV}0j?JWMwqA9vDL;9(Lw)OwgxFu`Y@LbVvG*TX3qT_>JJ9TGHE!|w3ZrKdW~mE3~H ztHV<%b=dm&Ww#~hfYC~$dcMv%`<&^)#foNC`LyS#k~>W^eo*?g?^e@o_~99cFHJKk zj4TRE0Bz182)D50VyiSV8O?}itp62tUW`d$$YkQPX=LEh+#3V{6b3JR7E}%d7LQNB&MIeUv?3lVTN-LD`pR>zTFzGXd|uU> zh}TKIu5Uii!kEyK*5YjYk5*A-Monq5^_hq3wBVb1(H2izov||w1B1#8TwHHq#uC<* zrd^BQPsp=R^{B9->Cl#Pa+9c%*LPt8Zj1y6Fgivug~3Hzx16`C8@A2bq>eTXEU^Jl zVY~Am>VbVa$70h{E$e=*@#}k9goWYLeJ#BjP|)-mDwqW*;XpmKKxe3djoB*2b@P&( zlcK$-2K&P|5QHM&QVH>~s`S>S-bO0z92D0?Uim%erW0(N>B%seCxA^&*7lBEwQcWL zOo)0;2RM$E78jGL<6~t_*Dp?%ZdGAor>9|Hd!l0$ZKp9X;=}U^-e0n9cva_Hw`KUI~7KM(6!~BeHFw1hwmWsWrM% zcOq*nD0|p7ZBYfM<3-cKMMLq4*Wa705uQCo{gOU9${Q&r{t)xkdmM{EGyakp=-cJ{ zFXnyn;@tL!cjJ(pIup3_Qr536dqZzLL7Gd<^SwUA8=eV^WtrQa%FChkS#E72@0{(G~7rk@thx%3J7 z5orrL27Wnr)@hd1681ZKb*qX0`CN0~s)<3M|G@kNj+^jBl!e-fq-nsz$2LmZPzFYpHX3=5FW>N)3b#{Kl zS9N~$Rq@K!Gtd15!>bv%?ghc@EK!I)WF-y0j-sEtyQ&4Q#YxlpeXIsl_UYUZH|UE! zJd)_ayC01U<5q&PhCS{T zRN%EE9TjYO<3m1TxZp$Z@$pme7We8w#JhHURB5?U33%;!MD*G-D~aQ|2s&Dow%d1# zPzmF4pYanPrh){o3sb{yOC#S!7`V$li9mN}@wk%X`TN_Lw$9JlS}qSjQ|G9|_cxGn zoN_}#pry^#|1$2RvH^=%*hTrJAbTfPFJqP-f>@hvNRB2KWbD_n>ul~rew57qRrhTL z_EsTXPVNNh0UGiYMNnC0(3T?ak8=a1ol`C$8})5Fl7lFQPxP||D@7ibX3ShQFyf6uNae+DfW7K)@o8!9)59!vehT`s3OGH2Fagczr%%1v_j1 zNFuh^Sgn*AwH~w~Ymc!cVs=RT)3b_J@`swWQ=Cg{aT-rJ?UNB-fEb-GznT}{QlGgl z>e@Zex)Og=^kF*D++`**KM6W`;T5p8s0z}f*~iiaaseGCB-?1Ib1&yg z4+hDVycdl#YvIg1q>m_XJm-h&A=cLhP%!ATDlUgTQlR^whzcy29Y>m_ESzQ`D04G3 zu!6NL^2290cw_?^xUiRZ_{Px}Jf2|={ZuwFtQ+FWOa)bDzfYXA<}BSLVgZi^JsJuRb)OVP_y zCR6_Sq+uYc{F1N+kv-0SB5)81MCH68ZEAw=^TeA{bpGQEBORoV9IS=<0+H~TJf z;tc(c?)_o!6u7D59m}G6Gbwx!aT>9$q5R!+@t0%D3CiF5j$2YV*#ba!3Gj?DRd9L1 zlh-r5C9QRrI9%uZs`6rLg%OG3lH#cL!5Rw2G)Spaz`o7s?%*+_k z+Q+rMS-u2DPFb^&p-7lbzR6F=H&9C1e5R?Je3EXPA>&>1klIv3D9- z_(7Rt{mtPl%MzCdvc#4sQ*e0T!Q+GaHu$_JQL^zpzVV&~r{&AL)hr8J3gxR>1@X(X zp@~m{xbfxmo+<|oB`@|&*{`VvS($X`ax!?d4<=c!2~{O{UbgOx6->LXce8<%z#jE9d8aV%WA6NA$V2hz4zI0@hfVGMH~hw`nTm`eNdM5m26@BI`76=2GNHf4oas=u)|LsnA`=F zs;2CUSZgwt?aAsVR~u&C1&A|rV6arZk^1%sX{k#6(Qn+2m9Dub=m3Eiv$uF-mKbo0 zyk4UGpGmHnzDua$K=UBoBa9f+2CAyf2;-Gb|f0ZQL>C0TZwecE-ZtV z9cLFC;O}Sj51T*V&CyOH)dN$t{?ESulYs0j=3naccGR(TS@Ui@3BBy7fC zEh=V5{(kB9dCA#OD&gaH0c=OVydR7lUdHb`GssHf0v2>qze-l3eyCh*pk zITWA_4aTTPbKTw*p@wrIr2)nQfeHK^-QtBv-rVU+O_K}f+@BXto4XIY*7VoB>Nj)v zYxKgw6eE}`C2yzuHNE;n!5g1oqnrs^U_`|mQFSJ_nJMGqqM0N7yK3jCfq>Z@_>Q(` zm#&EdP_=0cL@<}2wDmI?M&VN%AjYds*JKH)B|0&qZF}d>`e8cQF2eI2gV-Yj;`p+PI z&TFTzVzO((13#BdSejOda?IqH(t%;w2}xg27xEybL2Tv_D_l_$UO*$xp!^Rxe1z=P z!c|Z2k4Q~_hn{fGzF}Xd8na%&JkiPSzF>aquA~Cq5w&v$9>}dn1sv!vgccd5>Pa6t z(2g6|!2y+%jn9;Yzi1{=kahBDL_`79{$}t2;5k+O5!NS;7aTcDFj|<=_p#zI0DTB# zlcehvpk^NhAexn(qI*+P(75pli710kYd9jPwep<%l(OlKZ_#VYCp2j)Gq>)RyH|oO zp=19*3<62HnBua5(XpPNhkbaG*|T5&Rr_U7p#fFyIM%9t8buu4ygIiA=Pmww0O;_kH{?mQcsMS)t!ae*_##ETDxTeZ9SgAAhy z5@()r4EG?134|!InloK3_0_rIq)qZ|mfLt2N^%4}aT2-1`QCas8)6Sq=njR<+EA#M zXWxr7Fs@9Mt99*il4)C4{k`N@=}Ay)i#^!Akgw8EOAlV}MDDb1FgzOewsWna<>6=S zlMg;Q1AJ5E_{~rEeRhh_epbjZOxDPAFm;SOPDV8($C>DWov#CT@B}FC>Now`k6T&) zXd%%Mgl1@x4o|X>|NEQ~hNd%526Due8E>dclt{8BPqGeARtk3sf2m+mWuTfqnOFT; zRhUIscP5>QpoDl-SHX-jV4ZP&IbkV1_S?Bq@lo;8DrSnDoXG$Yx=D5v_G zHo2pN1ucuSprYlnLvst6n?zw=ljb3@rZR=M#w@nc>Yl10LTP&65lY;TP(lJcMYK?i z8|42k2*M54lclMw^X2E4c(5i2(t!KaR#Q!G=wEh#>!ml)#-9RJ;~2-;r75#Y`959U z$SZe$B>$jdJ!W!p(E>MTn0rCFKT(ku zjx!h5E!{R`74a`N`bcw=ve<^9{B^+=R8oa+1!7oyNqsBzER+4et-PT)p-kzQB+~ZS!&q?E`^{ ztSP+wc~XKyZ~xiEaspxQ?_@Sa2-Ltno;Fw5?h3MQ_{B5IXbMJ;g-1Gin>%J<-=oE= zpZ;~cXMFNB%Ud_ItucZBk2F^k!;_3jPWrdk zh+|7)nz;C8Kt8H)dboAF?JVB`SHgip8pMxBqs+X(J&6T+_KQ1Rw3ZhktHrRtK_tkB zc|tU~n%)-!N?D_%{W(k(o~-KB7>56Tou5AyD~x(k)kcj`k_yV+kOjI=#Z%=C37~0w zWq&LLvWgTDMR6a;t4}gED~?-xTYJ?ML0D*g#C740{}xfwm4ol}!<-T|%UgbK=(Poz zyzj@tapOV;+CLAXY6vTeP|JAQlD_JrQEtHKknwJl5i}@+jj!G^0RQYcG&KWxSA9^( zOqmG^3ZK|iHhZ~a)2@}`{SmYj^3*e}daduG3DZpvc~a>=S=ldT3oa<2JUPCxG=k+h z%w8pnlv1Mh+|O#6K9MuE0R38j+^#~a$4SQ2AJztW&h8U5V(clQcNS&V`k@GS04BD# zL|~BHGWhY4?U~qYSDcp>Zm}-P-SARbF|06%4xTQDM8e>4aY05fNcvUrR}Q1)8{aQ6 z=^k}cJ$h|r9P#L^9xwimzQpA9Q#mGM#kT7}gZh>1lfjs>-ehpDxo99~!CLZwh49`P z$yML|Jx~Y>+c9Q^C(yl{McoN?H#BR_UY(NO#21i~2hjIK1rRwC3J<-&{L@W>Fjf4;oNDu|Uk0>MVQ2{yWQ{Ur%qqyo z_$NSa%r$+2Rtn7*(&OfQPbjMkaj#v%>z3SN2*?2-yQNL+Qy_qM1)^Xtr_uLpou5h7 zi$=eHCo@fnchKY$5Y}UT@Yspcz9&9{;~;-C3tuVaKUx+Zjn_u3*VjO9{tk~tps znpFL2qggcaehf|sFg$Nd73452fwzt(%A^;J@jnuE-X^!u-M+*cVTEJW2unA(HH9*y zS#?ir$g|(Yo)gY(35jC;+CTzzyBQo`U>dXlwRQ1VEbDc(J6X5yLbbK+;U7s%*`Rt* z)nq6iPqix&!Kl_`aOhc(Bj=#aCPf$}hPb<3(jKzEB_(y@gcHlW+zmn@v3?z}h6{k4^=SI; zbnxVDTLGB_ex59yg63w9hcFw1YpZ=dxF&zVN&$`+e2N_v{R3nCl9E>MYaTg&R#^>=v z+S+#G7@X+2oL`UM0T~co29OeUDw`{Br#+9Ye#J_GE&@{3(2xaY8aRFf@YRquaEBUp z@uG(pp&sy&81!q5_u3KPPDOm!(7>#r37zZo&b;5QpGTSdN!e4=-;`` z=W;p$U0R>*KyBi$kjf!9@&hit z4^&>qs|<5uf{S0laR#Dn5XrB2X)D&@56-41suM%(O7i)r7IOZNv|3{1CsT$Neen8I ztJ`?;&w6?RykFG1bBc@WYR;sm@MpcNQ*+&2z8&~0;e2q&<5z#avtRv*U2eL;hyano znL*xXAs8~Og;zP==v`o-eHpc$p+M{rP07KhWGbz@7~d;8*+(GR0pjLo6B{S3T12&t z+K5Hh6y8Y}G%(F^cHm&8y^}gUL-0?D7)>UfVtlsQrz!oxZJzLNR93Tj6Xz!%NA#kNK*& zC;0QqOts9M%)h-@$D1Cq`2pR@7of2x3Mhd1*tQaW_E`X-&jo_}-y7h7?AlbgT=|dwRnSyer?!;( z^d9GWPg9kAAA=_f#8Mtymn+{dV%6yu&|g~?EZolcY$WlH>GkxQhGh^&a}l(xr+ZpB ziavm(lN89;xq#5%PWnZ78>GDaQc>VfRgKVmxE_-A<6H=7Lx4vHH_20^pKGbrD|J}WRT;xtT zgII_eDnF>L0!7bTl^Ml z6<=y4;^9N?2mY6Y#tkkdqz4I|D5#F2pBUFKoX#>}F20|7;CM8A6+sUAoY46Ao0;q3&Js2WXhX-g(S+IV4oH24_#Rh`#2x}t>n68}&)RIG3 zkDuH@F|-m^hh6y-lq5<5+4AsA4qH05k!gd|x}RQtKFBuyL3{1VlviXmg0NXtp-D0O z`&OCw3mH~%b4d_uEfcJ!YrXw%E^hdqGmk#(dfBA~_XaaUbfzMRln4nzUG@gHu9(Bo zY?6IWz4jLt-9!M`I}en*_SGJu%opd?U4$-=v*8#R7%(w4{j@v& z@o;OA9o_@!%WmETT8elK*U7uWwgMm)4(HI1iOv5Ia4SM%yd7MK0|+ z+uJJ|8Yc2UdZc>Mjss)Pm@l)aV1Qf-T*l)C`|}{VrAs!ZVK;~i8&m20c#Z5~it#Nb z?uvi}B~6W0-dt)y!KzR)ZpDPm)_<3>b~X;y12pGouY*mDY&q8FS9p|Ct_#W{IolTG zhQnN3hcgolCL`O8-_}2iSTCbXKX&Yit-Mh=wZqmq;WDHau;b%FOV72*(lXAEo6DO1 z%f|WExRCf&4|0qEs4pQH1V*2l59Z@JLY1K4iiKUbG<_EkL<0{fw7VK`O@k^h>7fu@ zBk!`{dPwfq(f_<>gq`J>J!pA65R`)&2S8lxJ)5b(o-iw>LR;S8ab3)!nTHLT=u8mj z-aQ%WFZWLgbhiwlL5jreB~Jv^>=_HN8W`5JZ(Z_R7GuWuq?)wmm=cKS%o}I40Ckn~ zijxMSc@LaB?7W?qnc;Rj}N*Fg9uD7_`2DvAV(Bu&)f|{&x zFsj6Q3%@?miCq=&4Q28HLbPO4j-&d7FMzS;ihzR35mlTZQQ{b3nVdW!>r4zbVR zYy=DoKv8kK*V+G#I=PrxK{Yv&1QMD*pdez;=QnlBDq8TH#j`Wo1+mE`HurN%TEO%I zO0n(n6X#|ca;gvrBTt~UdBDYsCZ>Y9)x63gr2)J=5_e zuF^AtRJ7)`C5y+V&!{vyz~$9~oPlojQzUGf6HjT4l|G@@wl5ImDo?rK z-hPs$=ZwpIo@v*FDnA&&anOLV9ryZwVr|MJ3>J`1H5L7PENd-GZf z5s_XmjO-6I6H*0S8<3`x%4~Nne6an`g^`8M4^UCwaO^^e+VMc)%m(F~^l|BRvLpsy z8A$^$dt`0J@lPYGh=&^x>{`}+73W5;{zy?K6wHM~Z#rSM(897pHFecd7Ur#?T71Sk zV{!}h5~l%v-B>E4c7=Ci-}_&ktjVD?JWomjj1pg+NP~#9?Aa9d`*$PB=@OKNRp~>P$G;kPAg%XMIfG~W82go8Isz(&+s~@wW_J#b^%m&$m zuy;b{@7{&SmJl@~PIhNlu;Gk>?%dob>~8(Q<5AGp>IZ$Ld2kkj!i!Vd7-dBACG$a# z?UqVix*dI^BK9>lLuCPS2TfHW@SHB~#mMl8|8VJu{MDL$liLV++9{{S*=oR2VB&|H!9?#X=+ zJ)JCYQAkn(Ld^wY`4-fUpWIt}a=rLXU}DHP=o6+Ldwrc=l^*|1+a`%Kzy9Ll{lE%U zPmJIfUCzG+|3v)2qI5u{^6Mb-CtkA;qaWRUaVd4%SQmF?0nZaik{jryf61M~Spno9k`_@7SR1{CSR(peE%^b)78mspG z>rd)CiRM?ZBSA=5pOU>&g5w~)Do)p7)D8g*~D}`!Nz@&~zEG5bIIN$G*nxoTcC$Sljhe?1C zhAWG}ug7C~4V1va#RFo3d!T0B)L453^f5orO@#rD%f`ZlPwXzO)@On@HORqtehkTi zyZY2umB#gs%d-l_Q5dYk(!WMwnrmWZ$I$ws zDCh$I4?u8z^2%c17GEX@$94!AO=!?26Wr1U?D@$g4<9~*f26C=?L9XvU!ouFg0@I^ zt}|70lXazJvRxx8m_U7EIEQMI&oUHbzF;?nUHK%iB{<_C`g!Qu{VYpqR7EK|nj*j3?15&>3ZE0y%Ov!jz z0<%JHjT`}Vw2mvT<71xsD^rjoAjAbTM!OG22R{rY-ycQfaVv|$azBq=qAD|6v((tS znkzt89IsiVO&VrUqXDBUe?Pn;w-C=r;?g%jA#cu?evCjy%K0P5JkH81dyjb>4Hdz$ zL!&NTH1<7Hbn{gE$2o5|c4nH=u5!nAMgbrr$$EwX>SQJPy02`5tfHPf1I!u`Lh_JC zxDTWHCAS=*1q*37n+ib~j`8KbrBLZbnDhj2Dql$|Ar@*yGq3i76o?+RS@@qB2IX^b zF489d5o_2GS(`aH7KY)#Cd}@i+U*LgP45JC4rKI#C)L$s|y1($9mF!^_E3k#RvJ zs#0&sZ}H@wTsc^#<-TPpuT!*O>UUK-_O&vgHh@urD0vc~+h1kf4y@if9Qxu{&a}W5 z>{GYykMumKNQf}Hq+VhOE=`=1gXGt-3Tm%e}Nz z7L;60J}659!Ub2wu*Y$ELh1K&O(f!k!YF#;@)F9S@dBf1Ujx;+YMJ^wZn?59bQ^O{ z8o@sRnnEtB@wRT>-3G3(nWX#-t|AG4zY;OcGg!MHdg)(koTiq#6u3D_lb*-gtn{ci z_7-}5D9|4#vd{zXO4op=qVqTzW}@>Z!Ln#HfQs6Vl`KCzj*7l8j@cTaX}=f@_J%O} zP~p3bVYKarpfha+Zj8LQ?aY+bZ5Lkfx;>)2+8NyHGgiL}dO#nbj2|$h`rUy2)O$@{ zv*snbe1+xA2obwOYm7OZc;>#VRq{>EH0RwxM(tU!Bo1MuD6n14k4>bTLLiBi7y@vd4Np?fC+(m z&vuMYId9BvIEwo7^)cLQ)+KyU`n6FJ_D9!#$k%;A%;R)P2J`A5QmiJ1+iKjx9Uxk% z%`ecGzx}~!d66`~;P>vXtAvW>E>-DA4{SbT9QW$gz=_3R`$BF-O;%ybphKFFEbk|K z7g1ULpft|T)tvr0GZ4JEU{`czKNn|EGi(24tTlrJ!~70Q7Hf0Y?VoxZ3R0I9C#C#O zby6~PGt{uQcoT(vJi=86sB>EiJ|v(JMA=Sf4FzBR&+8I|aGD$GS2r@DH)J%f5~g~{ zy2Gx7@*x%z0g~J;9ZRPh`~u#a0i7{_%5LjBfyz5zUWM+&SL8WXE|7yAPR(ZlyWTu9 zA&OxthzhE?*}kKbU%L%jzV5!(x*=2}kl+3_F5&aPsVArQU||2&>C&frU5jFq ztP}uB&IKrUYyh{bO2q8~5qyUcg;95=hDA0XcRKZ#h|1_olkFtD`arJ>YE?*b%Ou8s zRuEV>32(T#KzQM&@g*gzW-rw%V-Jsh_I}Z;o2U<-p7lJidl~8Do91CcYc;(wEDA^e zGS2HiWc@Qg<#7xyloHgSCBtHT{;aI7BT-u5`27>y5}5)-K*YD(eRK5qK0^vLEDAUa z+z0rhw}(eke@_PGs9aoF!c`*j?TUgzNz%u)L4-|w+o|B6(~S}FU~vvfj7rJ5rOf9y9 zuM03ViW~Vl7yyPy9_-7bfJ7Kx!!Tc4g*0gSCx5K~-Fv|L?Py-qq6wF0yIUbRw4xV3g*Xg#-pPYRA@MZPR!WxeMZ2tQwQ9rk@IuI6`&H{UcPMs8M1=9#RSNz zSmGP=!DDVp%Y=xBsE>AKq>t(HR9&Z`k;v8M{oc=<5gPF8ONc(Z4h#k=EomLaZ>Z#N z)^9y&X^k3j8n!w!3oUCp8ufm4bm5J|7DoWzT(c!&`VWU4MyaUG{V9 zsgC&=3v!4Cl%G2}$oy-nl2F#|$LIEmkW!;=XQ#h>U_w6UG66CNgl_SnHN&nWlxXY)=`|!{`6rdB6GXW$AF^*J zR=*2Tt11{nORpSFeV&m{I7v#_`lvo<80q9caXj9)qZGSQA|R5XkgQ{`;a5D{(d>a; zUt%!Sun;lXWCH<|(gSuCh+d`y_q7Wt%hXA!%<1HTla7eMa=4ls4>B}crZkkAP9oQb zjDKM{pHW>ykjgnA{=v%4htYbYeL__8hw1fy?-}yu#P=8v{$-Q!+fz43Vz(TfL=k9z zsf|O!iiFdz3#W^6=hZd`#=(6GL}+!U+96fx{rZqku1wq^fu@X~Kc>i(!h3z8=XV~# zNQ=po#&mN{9>V>_R=$Iz;=k+qR*ur%mCL?r~+MYJ6!Do}fK zu$Fx{QKt&pZRif8(u)WW=2hY5)DEkQl!76Sk*Xz7YO`P-ZAelW^l4 zB{iBt=7&hx-h7j_Et;Sg^+#*TpxvOcqgg*zX#Vu{0~fP$Cdf0W^*h08zj$Isc#4007nH^TUjeXhx6VrY6A%0|O;^8!4}6?qkNZ zZgM3U{NoaGpi5r`!9XcHgfZ6?`3A#ufsGFbu+^6g5FtKn2m0c%y+tDpM7HTSZQgJs$~ZZb77k zvg<|ZL1DFxieuU86JP1_&M0qLS$<_LG)A) zu{p=~fl%rO@Ru%s`0t{+=kW(6R*euPD!AEo zrK-zX!Z*KvrH>Seh_njI_e8vd@}(l!APJj$$%O{aP)lYqrV9cgrgcfzmN*(yHjkl( z8s?_E*{dEP!)`^D9m}bo>hH_Q8;GYH6b(w8yVIScwV(X98UhZ2|K@DaXkqFA5dey!>~XB)wED@Q8+mZ+FR*7!iaV@l)np zJO7~k8nJ6EDYT?%?c~J{q{`eY3dY`5KO1VtwWS`@wZFa#@nYQ<)6by7Qj!O~D8vK69ehAP*5tzU^_mN~4AuKid5y zm#ldxgzQ(DU7-bwq$fv>t(M_y1B&>zsKbo~)VI+}vCU9dih_NY~Qar&&Rgd#54v zRb11Ya$zg2T9CMUBWI*Vj$lY`hVN`Hn-3F=g*LQ57tT7^XCaw!kkH>Lo;}}D?xJgj z5N2nh_I@3rA>LFYwZwmC&k)^_XbC$|5D(_V#H-REw;Ou7vDa^8B7g z@q$)M8l(MngEGZQ$ph{8M@w1$KSZ&<8BM4+1V5LI(zOyDq7+S)3w_+ne_AwVyF_wES|j9+h<{?cJ#K+KdC7_etNLqGk*Cr zfm2%{g3b8qbGXFpHK?A3WExhCDN&m1m-}Unvkz;08?0$_`AIvC+^qC|LgX6bntO#w z&wRz4%N~{SkG%LhBm>_L_L-S}RH_$lr=It}T~@)H5O zLjHvY1GWy2xTG(?ja|nwFc}09m8V56EuH@g@3}lOpz{;3!zF^jltHQi2oFZWqIBr> z#^TkW-CqOkWCCHBt|QmL+<&Z;YQem-J&jsnZQ{|uSqw5DbU-g|A*Bo3>05K9dTGCL_N8) zpXHDIO4qbhrFQ@c6y(=`@Em^a!9O`~7dnZ~FPG6`BjsqDX)S9U>(xj2+LHz(efC#> zojbSiUqXRz3(+~}@PHJAh>ZINBt^cBaaRspdd1R{q1Y!nNKen2Pq zPi`uU%*1@hqMVwlepEBLb+vx8dbM?%<9X}kQ%67aVXJGmvE!8E?X-FSWRI!(Ik^hC zT?e(c@mp;U{lX}bWOFup%)S4c$f$P$&!GU2JWxU+0%(jRy`FozHwVH@s6|Xp+GiKO zpJj2DPc&sbIb8ZB6i!yrm`vzMw=CQDnvY=-JEvQYi6(2OV|{(6sU@$dx7La6yxSxKVv6FyN2an3o!qTMsZ^Xp?A4`hY(1)t8f zT@}uw*ppz-V4xh_zn%>2g1G++T9W~04AkC&g(S}xOr7E!IXo)DFe&0?o^+RXS6qPc z0{-v^%Y#F{3h)VW011;23y5Ryex_hRy9E4~z_sWf{Cnl{@h01QHiUW<(dR8Z)xR%L zzVI5+(FyE!02DRc22rvmSvcl*n37nZ$A-+)_+P67V%#%DH)X z0;2ui8H-Z}m^@!j^xLOkWM+J@&u6a808GN#p&?}5i+|So?3|U_g04d5sk}+4cDOcA z!*fPS5tkL6Aw2l5Cki{9-jH{W!2YX84*0G721Wo~bf|eb7^H^<5Z#F8QJ~dD=Pekb zk^=nOTmaZ|aQixaAW481F&DDMpa9HXnyekB#7q2RGCS>!;<7*}ZlE3nmfF!-4dFR3 zC0uk{(Ijkq-e z&{{tfgEiMvUz-tj073QI~QUZ)sySf0j)yY>f8a&m&Z?a@&Be6$`0ir<$JWBRA zW`jVvz-sSzAFyF0`3!2m_YUrkJ8%=FM5J~8pVwmnf~i*N&T3kV^~FA=>-6Vsj~ua zGGM60ltFphde4%LQyU!}{y2k@OLu(_JSwgv{}ej5GFG7tP| zLSzc-MS_8c7STO04?_KnFq@TxzEiQ-FLBi5XN~PCD@DGWQr-- zK|*QK=ji_$abEbzQy576-(9Eg^Hx9VYgfmlgjgCARp6>&$kdIr!EwqPSZFPw| zA|o?#CjlNJb?D6-lgpc`fa(ui0#%f_K?Gs7Gt3L#jQYuHI zEtN=~J$Gm*j(_oT;5RS~CHhMO0~w#6W}{8gi-3(VgGtWRph!HXnG#UsJbx=#XpZW+hD m{=e@nxLg0vc>JCapf8M#`1*D=nt-D+L_<|a<%5!C(EkF(DGks7 literal 0 HcmV?d00001 diff --git a/public/logo-wordmark-light.png b/public/logo-wordmark-light.png new file mode 100644 index 0000000000000000000000000000000000000000..3da485900daab21aad2ffae3ce2b3844b04cdf9e GIT binary patch literal 22027 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 literal 0 HcmV?d00001 diff --git a/public/logo-wordmark.png b/public/logo-wordmark.png new file mode 100644 index 0000000000000000000000000000000000000000..8c470b02960ac24880ccf021c2bdda087fb2cd2e GIT binary patch literal 22191 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 0. The + * products already have their stock and this must not add to it. + * + * It re-sends each product's EXISTING price, cost and tax, because the same + * call updates pricing. Sending zeros would wipe the prices while fixing the + * category. + */ + +const BASE = process.env['FIESTA_URL'] ?? 'https://fiesta.nearle.app'; +const WEB = `${BASE}/live/api/v1/web`; + +const apply = process.argv.includes('--apply'); +const tenantArg = process.argv.indexOf('--tenant'); +const onlyTenant = tenantArg > -1 ? Number(process.argv[tenantArg + 1]) : null; + +async function get(path, params = {}) { + const query = new URLSearchParams( + Object.entries(params).filter(([, v]) => v !== undefined && v !== ''), + ); + const response = await fetch(`${WEB}${path}?${query}`, { headers: { Accept: 'application/json' } }); + if (!response.ok) throw new Error(`HTTP ${response.status} on ${path}`); + const payload = await response.json().catch(() => null); + return payload?.details ?? payload?.data ?? null; +} + +async function post(path, body) { + const response = await fetch(`${WEB}${path}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify(body), + }); + const payload = await response.json().catch(() => null); + return { ok: response.ok && payload?.status !== false, status: response.status, payload }; +} + +async function tenantsWithOrphans() { + const seen = new Map(); + for (let page = 1; page <= 20; page++) { + const rows = (await get('/tenants/getalltenants', { pageno: page, pagesize: 200 })) ?? []; + const list = Array.isArray(rows) ? rows : []; + for (const t of list) if (t?.tenantid) seen.set(t.tenantid, t); + if (list.length < 200) break; + } + const out = []; + for (const tenant of seen.values()) { + if (onlyTenant && tenant.tenantid !== onlyTenant) continue; + const groups = (await get('/products/getallproducts', { tenantid: tenant.tenantid })) ?? []; + const products = (Array.isArray(groups) ? groups : []).flatMap((g) => g?.products ?? []); + const orphans = products.filter((p) => !p.categoryid); + if (orphans.length === 0) continue; + + /** + * Where each orphan already sits. + * + * `getallproducts` is tenant-wide and carries NO locationid — the first + * version of this read `p.locationid` off it, got undefined, sent 0, and + * every repair came back "missing required field(s): locationid". Nothing + * was written, which is the one good thing about that failure. + * + * The outlet matters beyond passing validation. Import writes a + * productlocations row, so naming an outlet the product is NOT on would put + * it on that shelf — silently extending the product's reach as a side + * effect of a repair. Only an outlet where it is already stocked is safe: + * there the upsert lands on the existing row. + */ + const locations = (await get('/tenants/gettenantlocations', { tenantid: tenant.tenantid })) ?? []; + const placement = new Map(); + for (const loc of Array.isArray(locations) ? locations : []) { + const rows = (await get('/products/getlocationproducts', { + tenantid: tenant.tenantid, + locationid: loc.locationid, + pageno: 1, + pagesize: 500, + })) ?? []; + for (const row of Array.isArray(rows) ? rows : []) { + if (!placement.has(row.productid)) placement.set(row.productid, { loc, row }); + } + } + + out.push({ tenant, orphans, placement }); + } + return out; +} + +/** + * The category to file a product under. + * + * `gettenantcategories` is synthesised from the categories the tenant's own + * products already use, so it is the tenant's real answer rather than the + * master table's — which is unscoped and, for the tenants seen here, offered a + * category (1001) that the app does not browse. + * + * Refusing rather than guessing when the tenant has none: a wrong category is + * findable and fixable, but writing one at random across a live catalogue is + * not something a repair script should decide. + */ +async function categoryFor(tenantid) { + const rows = (await get('/products/gettenantcategories', { tenantid })) ?? []; + const usable = (Array.isArray(rows) ? rows : []).filter((r) => r?.categoryid > 0); + return usable[0]?.categoryid ?? null; +} + +const work = await tenantsWithOrphans(); + +if (work.length === 0) { + console.log('Nothing to repair — no product is missing a category.'); + process.exit(0); +} + +console.log(apply ? 'APPLYING repairs\n' : 'DRY RUN — nothing will be written. Pass --apply.\n'); + +let repaired = 0; +let unchanged = 0; +let refused = 0; + +for (const { tenant, orphans, placement } of work) { + const categoryid = await categoryFor(tenant.tenantid); + console.log(`${tenant.tenantid} ${tenant.tenantname} — ${orphans.length} to repair`); + + if (!categoryid) { + console.log(' SKIPPED: this tenant has no category of its own to file into.\n'); + refused += orphans.length; + continue; + } + + for (const p of orphans) { + const label = `${String(p.productid).padEnd(6)} ${p.productname ?? '(unnamed)'}`; + + if (!p.productbrand || !p.catalogueid) { + // Not imported from the global catalogue, so brand+catalogueid cannot + // address it and re-import is not available. Says so rather than + // reporting a success it did not achieve. + console.log(` ${label} — SKIPPED: no catalogue reference to re-import from`); + refused++; + continue; + } + + const at = placement.get(p.productid); + if (!at) { + console.log(` ${label} — SKIPPED: not stocked at any outlet, so there is no safe row to repair through`); + refused++; + continue; + } + + /** + * A product with no price must not be made visible. + * + * Repairing the category is what puts a product in front of shoppers, and + * the app has no price floor — `GetProducts` filters on category and outlet + * and nothing else. Fixing a product priced at 0 would not "restore" it; it + * would publish a free one. Idhayam Sesame Oil 500ml (7083) is in exactly + * this state, priced nowhere, and it wants a price before it wants a + * category. + */ + const price = Number(p.retailprice ?? 0); + if (!(price > 0)) { + console.log(` ${label} — SKIPPED: no price set. Repairing this would list it at ₹0. Price it first.`); + refused++; + continue; + } + + if (!apply) { + console.log( + ` ${label} → categoryid ${categoryid} (via ${at.loc.locationname ?? at.loc.locationid}, price ₹${price} unchanged)`, + ); + continue; + } + + const result = await post('/products/importcatalogueproduct', [ + { + tenantid: tenant.tenantid, + // An outlet the product ALREADY sits at, so the upsert lands on the + // existing row instead of putting it on a new shelf. The category + // itself is written to `products`, which is tenant-wide, so one call + // fixes the product everywhere. + locationid: at.loc.locationid, + brand: p.productbrand, + catalogueid: p.catalogueid, + categoryid, + subcategoryid: p.subcategoryid ?? 0, + // Zero, so CreateProductLocation writes no stock ledger entry — it only + // records stock when quantity > 0. These products already have their + // stock and a repair must not add to it. + quantity: 0, + stocktype: 'in', + status: at.row.productstatus || p.productstatus || 'Active', + // The product's OWN current values, re-sent unchanged. The same call + // updates pricing, and the location upsert sets productlocations.price + // from retailprice — verified equal for every product being repaired + // here, so this round-trips rather than overwriting an outlet price. + retailprice: price, + productcost: p.productcost ?? 0, + taxpercent: p.taxpercent ?? 0, + }, + ]); + + if (!result.ok) { + console.log(` ${label} — FAILED: HTTP ${result.status} ${result.payload?.message ?? ''}`); + refused++; + continue; + } + + // Verified, not assumed. The whole reason this bug survived is that the + // call that was supposed to fix it returned success and did nothing. + const groups = (await get('/products/getallproducts', { tenantid: tenant.tenantid })) ?? []; + const after = (Array.isArray(groups) ? groups : []) + .flatMap((g) => g?.products ?? []) + .find((x) => x.productid === p.productid); + + if (after?.categoryid > 0) { + console.log(` ${label} → categoryid ${after.categoryid} ✓`); + repaired++; + } else { + console.log( + ` ${label} — NO CHANGE: the call succeeded but the category is still 0.` + + ' The backend fix is not deployed.', + ); + unchanged++; + } + } + console.log(); +} + +console.log(`repaired ${repaired} · unchanged ${unchanged} · skipped ${refused}`); +if (unchanged > 0) { + console.log( + '\nProducts reported NO CHANGE need the backend fix deployed' + + ' (services/productService.go — re-import must call UpdateProductCategory).', + ); + process.exit(1); +} diff --git a/scripts/appgap.mjs b/scripts/appgap.mjs new file mode 100644 index 0000000..b5beaff --- /dev/null +++ b/scripts/appgap.mjs @@ -0,0 +1,193 @@ +/** + * Why the customer app shows fewer products than the console does. + * + * node scripts/appgap.mjs + * npm run appgap 1135 1166 + * + * Straight at Fiesta — no Hasura, no admin secret, nothing to configure. The + * first version of this went at the database through Hasura, which was the + * wrong instrument twice over: it needed a secret, it 404'd on an admin API + * that is often disabled, and it answered a question about the DATABASE when + * the question is about what the API returns. This calls the very endpoint the + * app calls and compares it with what the console can see. + * + * ── The three filters ──────────────────────────────────────────────────────── + * + * `getproductsbysubcategory` drops a product for one of three reasons, and none + * of them is an error, logged or visible from either end: + * + * A. `WHERE a.categoryid = 2` — not optional (`productRepository.go:865`). + * A product in another category cannot appear, whatever else is true. + * + * B. `WHERE pl.locationid = ?` on a LEFT JOIN to `productlocations`. No row + * for this outlet means the join yields NULL and the WHERE drops it. Being + * in the catalogue is not the same as being on a shelf. + * + * C. The grouping in `GetProductsBySubcategory` collects products under each + * real subcategory of category 2, then sweeps up `subcategoryid = 0` as + * "Uncategorized". A subcategoryid that is non-zero and NOT a subcategory + * of 2 matches neither and vanishes — present in the SQL, absent from the + * JSON. + */ + +/** + * Through the console's own host, not Fiesta directly. + * + * `app.nearledaily.com/fiesta/...` is the nginx proxy the deployed console + * already uses, so this script exercises exactly the path the browser takes — + * if the proxy is misconfigured this finds out, where hitting fiesta.nearle.app + * would quietly work and prove nothing about production. + * + * Override with FIESTA_URL to point at the backend directly or at a dev server. + */ +const BASE = process.env.FIESTA_URL ?? 'https://app.nearledaily.com/fiesta'; +const WEB = `${BASE}/live/api/v1/web`; +const MOB = `${BASE}/live/api/v1/mob`; + +const [, , tenantArg, locationArg] = process.argv; +const tenantid = Number(tenantArg); +const locationid = Number(locationArg); + +if (!tenantid || !locationid) { + console.error('Usage: node scripts/appgap.mjs '); + process.exit(1); +} + +/** Fiesta answers under `details` in most places and `data` in a few. */ +async function get(url, params) { + const query = new URLSearchParams( + Object.entries(params).filter(([, value]) => value !== undefined && value !== ''), + ); + let response; + try { + response = await fetch(`${url}?${query}`, { headers: { Accept: 'application/json' } }); + } catch (cause) { + console.error(`Could not reach ${BASE} — ${cause.message}`); + process.exit(1); + } + const payload = await response.json().catch(() => null); + if (!payload) { + console.error(`Malformed response from ${url} (HTTP ${response.status})`); + process.exit(1); + } + return payload.details ?? payload.data ?? null; +} + +/** + * Every product the tenant owns. + * + * `getallproducts` answers `[]models.Tenantproducts` — `{tenant, products}` + * groups, not a flat list — and under `data` rather than `details`. + */ +async function allProducts() { + const groups = await get(`${WEB}/products/getallproducts`, { tenantid }); + if (!Array.isArray(groups)) return []; + return groups.flatMap((group) => group?.products ?? []); +} + +/** + * What is actually listed at this outlet. + * + * Paged, and the page size matters: the default is 50, so a shop with 200 + * products would look like one with 50 and every product past the first page + * would be miscounted as "not listed". Walked until a short page comes back. + */ +async function locationProducts() { + const rows = []; + const pagesize = 200; + for (let pageno = 1; pageno <= 50; pageno += 1) { + const page = await get(`${WEB}/products/getlocationproducts`, { + tenantid, + locationid, + pageno, + pagesize, + }); + const batch = Array.isArray(page) ? page : []; + rows.push(...batch); + if (batch.length < pagesize) break; + } + return rows; +} + +/** Exactly what the app asks for, so the comparison is against reality. */ +async function appView() { + const payload = await get(`${MOB}/products/getproductsbysubcategory`, { + categoryid: 2, + tenantid, + locationid, + }); + const details = payload?.details ?? (Array.isArray(payload) ? payload : []); + return Array.isArray(details) ? details : []; +} + +const [products, listedRows, groups, subcategories] = await Promise.all([ + allProducts(), + locationProducts(), + appView(), + get(`${WEB}/products/getproductsubcategories`, { tenantid, categoryid: 2 }), +]); + +if (products.length === 0) { + console.log(`Tenant ${tenantid} has no products at all — nothing for the app to show.`); + process.exit(0); +} + +const listed = new Set(listedRows.map((row) => row.productid)); +/** + * `subcatid`/`subcatname`, not `subcategoryid`/`subcategoryname`. + * + * This read the long names — the ones `getproductsubcategories` does NOT send — + * so every entry was `undefined → undefined`, the map collapsed to a single + * junk key, and check C below could never match. The footer duly announced + * "(none returned)" for tenant 1147, whose category 2 has six subcategories. + * A diagnostic that is confidently wrong is worse than one that is missing. + */ +const realSubs = new Map( + (Array.isArray(subcategories) ? subcategories : []).map((row) => [row.subcatid, row.subcatname]), +); + +const inApp = new Set(); +for (const group of groups) { + for (const product of group.products ?? []) inApp.add(product.productid); +} + +const buckets = new Map(); +const examples = new Map(); + +for (const product of products) { + let reason; + if (inApp.has(product.productid)) { + reason = 'OK — the app shows this'; + } else if (product.categoryid !== 2) { + reason = `A — categoryid is ${product.categoryid}, the app only asks for 2`; + } else if (!listed.has(product.productid)) { + reason = 'B — not listed at this outlet (no productlocations row)'; + } else if (product.subcategoryid !== 0 && !realSubs.has(product.subcategoryid)) { + reason = `C — subcategoryid ${product.subcategoryid} is not a subcategory of 2, so it is dropped`; + } else { + // Everything checks out and it still is not there. Worth its own bucket + // rather than being folded into one of the above: a wrong guess here would + // send someone fixing data that is already correct. + reason = '? — passes all three checks but the app still does not return it'; + } + const key = reason.replace(/\d+/g, 'N'); + buckets.set(key, (buckets.get(key) ?? 0) + 1); + if (!examples.has(key)) examples.set(key, { product, reason }); +} + +console.log(`Tenant ${tenantid}, outlet ${locationid}`); +console.log(` ${products.length} products in the catalogue`); +console.log(` ${listed.size} listed at this outlet`); +console.log(` ${inApp.size} returned by the app's endpoint\n`); + +for (const [key, count] of [...buckets.entries()].sort((a, b) => b[1] - a[1])) { + const { product, reason } = examples.get(key); + console.log(` ${String(count).padStart(5)} ${reason}`); + console.log( + ` e.g. "${product.productname}" — id ${product.productid}, category ${product.categoryid}, subcategory ${product.subcategoryid}`, + ); +} + +console.log( + `\nReal subcategories of category 2: ${[...realSubs.values()].join(', ') || '(none returned)'}`, +); diff --git a/scripts/appsweep.mjs b/scripts/appsweep.mjs new file mode 100644 index 0000000..35b6ed5 --- /dev/null +++ b/scripts/appsweep.mjs @@ -0,0 +1,246 @@ +/** + * Every product on the platform the customer app cannot show, and why. + * + * node scripts/appsweep.mjs + * node scripts/appsweep.mjs --json > sweep.json + * + * `appgap.mjs` answers this for ONE outlet and is the tool to reach for when + * somebody reports a specific shop. This is the platform-wide version: it walks + * every tenant, in every approval state, and reports the products that are + * unreachable no matter what the app asks for. + * + * ── The rule it applies ────────────────────────────────────────────────────── + * + * Read off the backend rather than inferred from responses + * (`controllers/productController.go:431`, `repositories/productRepository.go:858`): + * + * 1. `GetProductsBySubcategory` REJECTS `categoryid = 0` with a 400 — + * "Valid categoryid is required". It is the first thing the controller + * does. + * 2. The query then filters `WHERE a.categoryid = ?` unconditionally. + * + * Together those mean a product stored with `categoryid = 0` is returned for NO + * request the app can make. Not "usually hidden" — unreachable. It is still + * listed by `getlocationproducts`, which does not filter on category, so the + * console shows it and the shop believes it is on sale. + * + * Nothing else in that query gates visibility: there is no filter on + * `approved`, `publishedat`, `productstatus`, or stock level. A product with + * zero stock still appears. So `categoryid = 0` is the whole defect, and this + * script looks for exactly it. + */ + +const BASE = process.env['FIESTA_URL'] ?? 'https://fiesta.nearle.app'; +const WEB = `${BASE}/live/api/v1/web`; + +const asJson = process.argv.includes('--json'); + +/** Fiesta answers under `details` in most places and `data` in a few. */ +async function get(path, params = {}) { + const query = new URLSearchParams( + Object.entries(params).filter(([, v]) => v !== undefined && v !== ''), + ); + const response = await fetch(`${WEB}${path}?${query}`, { + headers: { Accept: 'application/json' }, + }); + if (!response.ok) throw new Error(`HTTP ${response.status} on ${path}`); + const payload = await response.json().catch(() => null); + return payload?.details ?? payload?.data ?? null; +} + +/** Requests that never succeeded. A non-empty list invalidates the report. */ +const failures = []; + +/** + * Bounded concurrency, with retries, and failures that are never swallowed. + * + * The first version of this caught every error and substituted `null`. Run + * across 262 tenants at a concurrency of 8, enough requests were refused that + * it reported ONE affected tenant out of three known ones — and reported it as + * a clean result, with no indication anything had gone wrong. A sweep that + * fails silently is worse than no sweep: it is used to close the investigation. + * + * So: three attempts with a widening delay, and anything still failing is + * recorded and printed at the end as an explicit gap in coverage. + */ +async function mapLimit(items, limit, fn, label = 'request') { + const out = new Array(items.length); + let next = 0; + await Promise.all( + Array.from({ length: Math.min(limit, items.length) }, async () => { + for (;;) { + const i = next++; + if (i >= items.length) return; + let lastError; + for (let attempt = 0; attempt < 3; attempt++) { + try { + out[i] = await fn(items[i], i); + lastError = null; + break; + } catch (cause) { + lastError = cause; + await new Promise((r) => setTimeout(r, 250 * (attempt + 1))); + } + } + if (lastError) { + out[i] = null; + failures.push(`${label}[${i}]: ${lastError?.message ?? lastError}`); + } + } + }), + ); + return out; +} + +/** + * Every tenant, from two sources because neither is complete on its own. + * + * `getalltenants` paginates and its `pageno` is 1-BASED — page 0 returns an + * empty list rather than the first page, the same off-by-one that + * `getlocationproducts` has. It carries 262 tenants where the status lists + * carry 142. + * + * `/tenants/search` is still needed alongside it: it branches on the word + * "pending" and queries `approved = 0` instead of a status + * (`tenantRepository.go:45-77`), which is the only way to learn that a tenant + * is unapproved. Suriya Store is one. + * + * Building from the status lists ALONE was the first version's other bug: R + * mart (1147) appears in none of the three, and a sweep that cannot see a + * tenant reports it as having nothing wrong. + */ +async function allTenants() { + const seen = new Map(); + + for (let page = 1; page <= 20; page++) { + const rows = (await get('/tenants/getalltenants', { pageno: page, pagesize: 200 })) ?? []; + const list = Array.isArray(rows) ? rows : []; + for (const t of list) if (t?.tenantid && !seen.has(t.tenantid)) seen.set(t.tenantid, { ...t }); + if (list.length < 200) break; + } + + for (const status of ['Active', 'pending', 'InActive']) { + const rows = (await get('/tenants/search', { status })) ?? []; + for (const t of Array.isArray(rows) ? rows : []) { + if (!t?.tenantid) continue; + const existing = seen.get(t.tenantid) ?? { ...t }; + seen.set(t.tenantid, { ...existing, approvalState: status }); + } + } + + return [...seen.values()]; +} + +const tenants = await allTenants(); +if (!asJson) console.error(`Scanning ${tenants.length} tenants…`); + +/* Pass one — the whole platform, one request per tenant. + `getallproducts` returns {tenant, products} groups, not a flat list. */ +const scanned = await mapLimit(tenants, 8, async (tenant) => { + const groups = (await get('/products/getallproducts', { tenantid: tenant.tenantid })) ?? []; + const products = (Array.isArray(groups) ? groups : []).flatMap((g) => g?.products ?? []); + return { tenant, products, orphans: products.filter((p) => !p.categoryid) }; +}, "tenant-products"); + +const affected = scanned.filter((row) => row && row.orphans.length > 0); + +/* Pass two — only the tenants that failed, so the outlet detail costs nothing + on a clean platform. */ +const detailed = await mapLimit(affected, 6, async (row) => { + const locations = (await get('/tenants/gettenantlocations', { tenantid: row.tenant.tenantid })) ?? []; + const branches = await mapLimit(Array.isArray(locations) ? locations : [], 4, async (loc) => { + const shelved = (await get('/products/getlocationproducts', { + tenantid: row.tenant.tenantid, + locationid: loc.locationid, + pageno: 1, + pagesize: 500, + })) ?? []; + const list = Array.isArray(shelved) ? shelved : []; + const hidden = list.filter((p) => !p.categoryid); + return { + locationid: loc.locationid, + locationname: loc.locationname, + shelved: list.length, + hidden: hidden.length, + // The number that matters to a shopper: an outlet whose entire range is + // invisible looks like a closed shop, not like a partial catalogue. + visible: list.length - hidden.length, + }; + }); + return { ...row, branches: branches.filter(Boolean) }; +}, "tenant-branches"); + +/* Coverage is part of the result, not a footnote. A tenant whose products + never loaded is UNKNOWN, not clean, and the difference decides whether this + report can be used to say the platform is fixed. */ +const unreached = scanned.filter((row) => !row).length; + +if (asJson) { + console.log( + JSON.stringify( + { + scannedTenants: tenants.length, + affectedTenants: detailed.length, + orphanProducts: detailed.reduce((n, r) => n + r.orphans.length, 0), + tenants: detailed.map((r) => ({ + tenantid: r.tenant.tenantid, + tenantname: r.tenant.tenantname, + approvalState: r.tenant.approvalState, + totalProducts: r.products.length, + orphans: r.orphans.map((p) => ({ + productid: p.productid, + productname: p.productname, + productbrand: p.productbrand, + catalogueid: p.catalogueid, + retailprice: p.retailprice, + })), + branches: r.branches, + })), + }, + null, + 2, + ), + ); +} else { + const orphanCount = detailed.reduce((n, r) => n + r.orphans.length, 0); + const blindOutlets = detailed.flatMap((r) => + r.branches.filter((b) => b.shelved > 0 && b.visible === 0), + ); + + console.log(`\n${tenants.length} tenants scanned`); + console.log(`${detailed.length} affected`); + console.log(`${orphanCount} products with categoryid 0 — invisible in the app`); + console.log(`${blindOutlets.length} outlets stocked but showing NOTHING to shoppers\n`); + + if (unreached > 0 || failures.length > 0) { + console.log( + `!! ${unreached} tenants could not be read after 3 attempts — this report is INCOMPLETE +`, + ); + for (const f of failures.slice(0, 10)) console.log(` ${f}`); + if (failures.length > 10) console.log(` … ${failures.length - 10} more +`); + console.log(); + } + + for (const row of detailed.sort((a, b) => b.orphans.length - a.orphans.length)) { + const { tenant } = row; + console.log( + `${tenant.tenantid} ${tenant.tenantname}` + + ` — ${row.orphans.length}/${row.products.length} products hidden` + + (tenant.approvalState === 'pending' ? ' [unapproved]' : ''), + ); + for (const p of row.orphans) { + console.log(` ${String(p.productid).padEnd(6)} ${p.productname ?? '(unnamed)'}`); + } + for (const b of row.branches) { + if (b.shelved === 0) continue; + const flag = b.visible === 0 ? ' ← app shows an EMPTY shop' : ''; + console.log( + ` · ${String(b.locationname ?? b.locationid).padEnd(28)}` + + ` ${b.visible}/${b.shelved} visible${flag}`, + ); + } + console.log(); + } +} diff --git a/scripts/checkHealthPanel.ts b/scripts/checkHealthPanel.ts new file mode 100644 index 0000000..1aaacda --- /dev/null +++ b/scripts/checkHealthPanel.ts @@ -0,0 +1,91 @@ +/** + * What the health panel would render, for real products, against the live + * service. + * + * Not a test — the tests pin behaviour against a frozen fixture. This runs the + * SAME functions the panel calls against whatever the service is returning + * right now, which is the only way to catch the service changing under us. + * + * npx tsx scripts/checkHealthPanel.ts + */ + +import { nutritionApi, __resolveBrand } from '../src/api/nutrition'; +import { facts, present, BAND_LABEL } from '../src/features/store-admin/healthScore'; + +const CASES: { label: string; brand: string; imageId: string; expect: string }[] = [ + { + label: 'Cadbury 5 Star 200g', + brand: 'Cadbury', + imageId: 'cadbury_cadbury_5_star_200g', + expect: 'a score, with a low-confidence caveat', + }, + { + label: 'Godrej Hit Spray (INSECTICIDE)', + brand: 'Godrej', + imageId: 'godrej_hit_spray_1101d017', + expect: 'NO score — blocked by the edibility guard', + }, + { + label: 'Naga Sooji', + brand: 'Naga', + imageId: 'naga_naga_sooji_100g', + expect: 'a high score, allergen declared', + }, + { + label: 'Aachi Baby Fryums 100g (a real merchant product)', + brand: 'Aachi', + imageId: 'aachi_aachi_baby_fryums_100g', + expect: 'known but unscored', + }, +]; + +const line = (s = '') => console.log(s); + +for (const testCase of CASES) { + line(); + line('─'.repeat(72)); + line(`${testCase.label}`); + line(`expected: ${testCase.expect}`); + line('─'.repeat(72)); + + const raw = await nutritionApi.forProduct(testCase.brand, testCase.imageId); + const shown = present(raw); + + if (shown.isEmpty) { + line(' → "No health score available for this product yet."'); + continue; + } + if (shown.isPending) { + line(' → "This product is in the catalogue but has not been scored yet."'); + line(` (service sent health_score=${raw?.health_score}, category="${raw?.category}")`); + continue; + } + + line(` SCORE ${shown.display}/100 ${shown.band ? BAND_LABEL[shown.band] : ''}`); + for (const good of shown.good) line(` ✓ ${good}`); + for (const caution of shown.cautions) line(` ! ${caution}`); + if (shown.tags.length) line(` TAGS ${shown.tags.join(' · ')}`); + if (shown.allergens.length) line(` ALLERGENS Contains ${shown.allergens.join(', ')}`); + else if (shown.allergensUnconfirmed) line(' ALLERGENS not confirmed — check the pack'); + const rows = facts(raw); + if (rows.length) line(` PER 100g ${rows.map((r) => `${r.label} ${r.value}`).join(' · ')}`); + if (shown.caveat) line(` CAVEAT ${shown.caveat}`); + if (shown.source) line(` SOURCE ${shown.source.label}`); +} + +line(); + +/* ── Brand vocabularies ──────────────────────────────────────────────────── */ +/* Our catalogue writes snake_case, theirs writes Title Case with a separator + that is sometimes a space and sometimes a hyphen. A mismatch returns + health_score: null — indistinguishable from an unscored product — so this + checks the resolution rather than trusting it. */ + +line('─'.repeat(72)); +line('Brand resolution: our spelling → theirs'); +line('─'.repeat(72)); +for (const ours of ['cadbury', 'coca_cola', 'brooke_bond', '24_mantra', 'colgate_palmolive', 'aachi']) { + const theirs = await __resolveBrand(ours); + line(` ${ours.padEnd(20)} → ${theirs}`); +} +line(); diff --git a/scripts/checkOptimiser.ts b/scripts/checkOptimiser.ts new file mode 100644 index 0000000..f4c9083 --- /dev/null +++ b/scripts/checkOptimiser.ts @@ -0,0 +1,89 @@ +/** + * The route-plan chain, run against the live optimiser. + * + * Not a test — the unit tests pin `routePlan.ts` against a frozen fixture. This + * calls the real service with real order rows and pushes the answer through the + * same functions the drawer uses, which is the only way to notice the service + * changing shape under us. + * + * npm run check:optimiser + */ + +import { optimiserApi } from '../src/api/optimiser'; +import type { OrderRow, RiderInfo } from '../src/api/types'; +import { + applyReconcile, + commitProblem, + dirtyRiders, + planFromSequence, + splitRoutable, + planKms, + reorderStops, + unplaced, +} from '../src/features/store-admin/routePlan'; + +const line = (s = '') => console.log(s); + +/** Real Suriya Store geography: the RS Puram branch out to four drops. */ +const ORDERS: OrderRow[] = [ + { orderheaderid: 1, orderid: 'N-1', pickuplat: '11.0118', pickuplong: '76.9456', deliverylat: '11.0284', deliverylong: '77.0120', deliverycustomer: 'Peelamedu' }, + { orderheaderid: 2, orderid: 'N-2', pickuplat: '11.0118', pickuplong: '76.9456', deliverylat: '11.0050', deliverylong: '76.9508', deliverycustomer: 'RS Puram' }, + { orderheaderid: 3, orderid: 'N-3', pickuplat: '11.0118', pickuplong: '76.9456', deliverylat: '10.9877', deliverylong: '76.9620', deliverycustomer: 'Ukkadam' }, + { orderheaderid: 4, orderid: 'N-4', pickuplat: '11.0118', pickuplong: '76.9456', deliverylat: '11.0183', deliverylong: '76.9724', deliverycustomer: 'Gandhipuram' }, + // No coordinates — must come back as unplaced rather than vanishing. + { orderheaderid: 5, orderid: 'N-5', deliverycustomer: 'No location on file' }, +] as OrderRow[]; + +const RIDER: RiderInfo = { userid: 9701, fullname: 'Meera Raj', contactno: '9000000001' }; + +line('─'.repeat(74)); +line('1 · SEQUENCE — send in a deliberately bad order, see what comes back'); +line('─'.repeat(74)); + +const { routable, unroutable } = splitRoutable(ORDERS); +const stops = await optimiserApi.sequence(routable); +let plan = planFromSequence(stops, RIDER); + +for (const stop of plan.riders[0]?.orders ?? []) { + line( + ` ${String(stop.step).padStart(2)} ${(stop.orderid ?? '').padEnd(5)} ` + + `${(stop.deliverycustomer ?? '').padEnd(22)} ` + + `${String(stop.previouskms ?? '').padStart(5)} km ` + + `cum ${String(stop.cumulativekms ?? '').padStart(5)} km ` + + `eta ${stop.eta ?? '-'}m actualkms ${stop.actualkms ?? '-'}`, + ); +} +line(` total ${planKms(plan).toFixed(1)} km`); + +const missed = [...unroutable, ...unplaced(routable, stops)]; +line(` unplaced: ${missed.length ? missed.map((o) => o.orderid).join(', ') : 'none'}`); +line(` commit allowed? ${commitProblem(plan) === '' ? 'YES' : 'no — ' + commitProblem(plan)}`); + +line(); +line('─'.repeat(74)); +line('2 · EDIT — move the first stop to last, which breaks the step numbers'); +line('─'.repeat(74)); + +plan = reorderStops(plan, RIDER.userid, 0, (plan.riders[0]?.orders.length ?? 1) - 1); +line(` dirty rounds: ${[...plan.dirty].join(', ')}`); +line(` steps now: ${plan.riders[0]?.orders.map((s) => s.step).join(' → ')} <- out of order`); +line(` commit allowed? ${commitProblem(plan) === '' ? 'YES' : 'NO'}`); +line(` reason: ${commitProblem(plan)}`); + +line(); +line('─'.repeat(74)); +line('3 · RECONCILE — the service repairs the step numbers'); +line('─'.repeat(74)); + +try { + const response = await optimiserApi.reconcile(dirtyRiders(plan)); + plan = applyReconcile(plan, response); + line(` steps now: ${plan.riders[0]?.orders.map((s) => s.step).join(' → ')}`); + line(` dirty rounds: ${plan.dirty.size === 0 ? 'none' : [...plan.dirty].join(', ')}`); + line(` commit allowed? ${commitProblem(plan) === '' ? 'YES' : 'no — ' + commitProblem(plan)}`); +} catch (error) { + line(` reconcile failed: ${error instanceof Error ? error.message : String(error)}`); + line(` commit still blocked? ${commitProblem(plan) !== '' ? 'YES — correct' : 'NO — WRONG'}`); +} + +line(); diff --git a/scripts/contract.mjs b/scripts/contract.mjs new file mode 100644 index 0000000..54e66b9 --- /dev/null +++ b/scripts/contract.mjs @@ -0,0 +1,434 @@ +/** + * The contract check. + * + * Signs in once, calls every endpoint the console reads, and reports what came + * back: the HTTP status, the envelope, whether `details` is an array or an + * object or null, how many rows, and — the part that matters — the keys on the + * first row against the keys `src/api/types.ts` says to expect. + * + * This exists because field-name drift is invisible until a real payload + * arrives, and it is the single most likely way connecting to the backend goes + * wrong. It has already happened once from fixtures alone: POS health returns + * `terminal_id` while the sales split returns `terminalid`, and an index + * signature on the type let the wrong one typecheck in silence. + * + * READ-ONLY. Nothing here writes. Every create, update and import is left to a + * person on a scratch tenant, because several of them are unscoped and one of + * them moves stock. + * + * Run: + * npm run contract (prompts for the password, hidden) + * + * Or, for CI, set NEARLE_EMAIL and NEARLE_PASSWORD in the environment. Neither + * is ever written into this file — see the note beside EMAIL below. + * + * Plain JavaScript on purpose: it runs with the node you already have, with no + * install step and no TypeScript loader in the way. + * + * The credentials are read from the environment and never printed, logged or + * written to a file. Put them in front of the command rather than in a script, + * and they stay out of your shell history if your shell is configured for it. + */ + +import { createInterface } from 'node:readline'; + +const BASE = process.env['NEARLE_API'] ?? 'https://fiesta.nearle.app'; + +/** + * `/web/pos`, not `/pos`. + * + * The `/v1/pos` group sits behind the terminal's session guard; the console's + * copies of the same reads are registered under `/v1/web/pos`. Sweeping the + * wrong one would report a surface the console never calls. + */ +const WEB = '/live/api/v1/web'; +const POS = '/live/api/v1/web/pos'; +const MOB = '/live/api/v1/mob'; + +/** + * The account to sweep with. + * + * The email defaults because it is not a secret. The PASSWORD is never + * defaulted and never written into this file: a password in source is + * committed, synced to every machine that clones the repo, and survives in the + * history after it is changed. It is read from the environment if set, and + * otherwise typed at the prompt below, where it is not echoed and does not + * reach the shell history. + */ +const EMAIL = process.env['NEARLE_EMAIL'] ?? 'care@nearle.in'; + +/** Reads a line without echoing it. */ +function askHidden(question) { + return new Promise((resolve) => { + const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true }); + const onData = (char) => { + // Stop echoing everything except the newline that ends the answer. + if (char.toString() !== '\n' && char.toString() !== '\r' && char.toString() !== '\u0004') { + process.stdout.write('\u001b[2K\u001b[200D' + question + '*'.repeat(rl.line.length)); + } + }; + process.stdin.on('data', onData); + rl.question(question, (answer) => { + process.stdin.off('data', onData); + rl.close(); + process.stdout.write('\n'); + resolve(answer); + }); + }); +} + +const PASSWORD = + process.env['NEARLE_PASSWORD'] ?? (await askHidden(`Password for ${EMAIL}: `)); + +if (!PASSWORD) { + console.error('No password given — nothing to sign in with.'); + process.exit(1); +} + +async function call(path, init = {}) { + const search = new URLSearchParams(); + for (const [key, value] of Object.entries(init.params ?? {})) { + if (value === undefined || value === null || value === '') continue; + search.set(key, String(value)); + } + const query = search.toString(); + const response = await fetch(`${BASE}${path}${query ? `?${query}` : ''}`, { + method: init.method ?? 'GET', + headers: init.body + ? { Accept: 'application/json', 'Content-Type': 'application/json' } + : { Accept: 'application/json' }, + ...(init.body ? { body: JSON.stringify(init.body) } : {}), + }); + let envelope = {}; + try { + envelope = await response.json(); + } catch { + envelope = { message: 'not JSON' }; + } + return { http: response.status, envelope }; +} + +/** + * A call that reports a dead host rather than crashing the run. + * + * A wrong `NEARLE_API`, a VPN that is not up, or one endpoint timing out should + * leave the other twenty-four results on screen — a stack trace at check four + * tells you nothing about checks five to twenty-five. + */ +async function attempt(path, init = {}) { + try { + return await call(path, init); + } catch (cause) { + return { + http: 0, + envelope: { status: false, message: `could not reach the server (${String(cause)})` }, + }; + } +} + +/* ── Sign in ─────────────────────────────────────────────────────────────── */ + +const login = await attempt(`${WEB}/users/applogin`, { + method: 'POST', + // `configid` is not optional: the lookup is `WHERE authname = ? AND configid = ?`. + body: { authname: EMAIL, password: PASSWORD, configid: 1 }, +}); + +if (login.envelope.status !== true || !login.envelope.details) { + console.error( + `Sign-in failed — HTTP ${login.http}, code ${login.envelope.code}: ${login.envelope.message}`, + ); + process.exit(1); +} + +const me = login.envelope.details; +const tenantid = Number(me['tenantid'] ?? 0); +const locationid = Number(me['locationid'] ?? 0); +const issuperadmin = me['issuperadmin'] === true; + +console.log('── signed in ─────────────────────────────────────────────'); +console.log(`userid ${me['userid']} · roleid ${me['roleid']} · issuperadmin ${issuperadmin}`); +console.log(`tenantid ${tenantid} · locationid ${locationid} · ${me['locationname'] ?? '—'}`); +console.log('login keys:', Object.keys(me).sort().join(', ')); +console.log(''); + +/** + * A tenant and a branch to probe the scoped endpoints with. + * + * A super admin has neither of their own, so one is borrowed from the platform + * list. Override with NEARLE_TENANT / NEARLE_LOCATION to aim at a specific one. + */ +let probeTenant = Number(process.env['NEARLE_TENANT'] ?? 0) || tenantid; +let probeLocation = Number(process.env['NEARLE_LOCATION'] ?? 0) || locationid; + +if (!probeTenant) { + const tenants = await attempt(`${WEB}/tenants/getalltenants`, { + params: { pageno: 1, pagesize: 1 }, + }); + probeTenant = Number(tenants.envelope.details?.[0]?.['tenantid'] ?? 0); +} +if (probeTenant && !probeLocation) { + const locations = await attempt(`${WEB}/tenants/gettenantlocations`, { + params: { tenantid: probeTenant }, + }); + probeLocation = Number(locations.envelope.details?.[0]?.['locationid'] ?? 0); +} + +console.log(`probing with tenantid ${probeTenant} · locationid ${probeLocation}\n`); + +/* ── What we expect ──────────────────────────────────────────────────────── */ + +/** + * The keys each row should carry, taken from `src/api/types.ts`. + * + * Only the ones the console actually reads are listed — a backend that returns + * MORE than this is fine and normal, and is reported as extras rather than as a + * failure. What matters is anything missing. + */ +const CHECKS = [ + { + name: 'tenants/getalltenants', + path: `${WEB}/tenants/getalltenants`, + params: { pageno: 1, pagesize: 5 }, + expect: ['tenantid', 'tenantname', 'locationid', 'locationname', 'status'], + }, + { + name: 'tenants/search?pending', + path: `${WEB}/tenants/search`, + params: { status: 'pending' }, + expect: ['tenantid', 'tenantname'], + }, + { + name: 'tenants/gettenantlocations', + path: `${WEB}/tenants/gettenantlocations`, + params: { tenantid: probeTenant }, + needs: 'tenant', + expect: ['locationid', 'tenantid', 'locationname', 'status'], + }, + { + name: 'utils/getappcategories', + path: `${WEB}/utils/getappcategories`, + expect: ['categoryid', 'categoryname'], + }, + { + name: 'orders/getlocationsummary', + path: `${WEB}/orders/getlocationsummary`, + params: { tenantid: probeTenant }, + needs: 'tenant', + expect: ['locationid', 'locationname', 'total', 'delivered', 'cancelled'], + }, + { + name: 'orders/getordersummary', + path: `${WEB}/orders/getordersummary`, + params: { tenantid: probeTenant }, + needs: 'tenant', + expect: ['total', 'delivered', 'cancelled'], + }, + { + name: 'catalogue/getbrands', + path: `${WEB}/catalogue/getbrands`, + expect: ['brand', 'product_count'], + }, + { + name: 'catalogue/getproducts', + path: `${WEB}/catalogue/getproducts`, + params: { pageno: 1, pagesize: 5 }, + expect: ['id', 'brand', 'product_name'], + }, + { + name: 'products/getimportedcatalogueproducts', + path: `${WEB}/products/getimportedcatalogueproducts`, + params: { tenantid: probeTenant }, + needs: 'tenant', + expect: ['brand', 'catalogueid'], + }, + { + name: 'products/getproductcategories', + path: `${WEB}/products/getproductcategories`, + params: { tenantid: probeTenant }, + needs: 'tenant', + expect: ['categoryid', 'categoryname'], + }, + { + name: 'products/gettenantcategories', + path: `${WEB}/products/gettenantcategories`, + params: { tenantid: probeTenant }, + needs: 'tenant', + expect: ['categoryid', 'categoryname'], + }, + { + name: 'products/getlocationproducts', + path: `${WEB}/products/getlocationproducts`, + params: { tenantid: probeTenant, locationid: probeLocation, pageno: 1, pagesize: 5 }, + needs: 'location', + expect: ['productid', 'productname', 'price', 'publishedat', 'status'], + }, + { + name: 'products/getallproducts', + path: `${WEB}/products/getallproducts`, + params: { tenantid: probeTenant, pageno: 1, pagesize: 5 }, + needs: 'tenant', + expect: ['productid', 'productname'], + }, + { + name: 'products/getstockstatement', + path: `${WEB}/products/getstockstatement`, + params: { tenantid: probeTenant, locationid: probeLocation, pageno: 1, pagesize: 5 }, + needs: 'location', + expect: ['productid', 'opening', 'credit', 'debit', 'closing'], + }, + { + name: 'products/getstockrequests', + path: `${WEB}/products/getstockrequests`, + params: { tenantid: probeTenant, locationid: probeLocation, pageno: 1, pagesize: 5 }, + needs: 'tenant', + expect: ['requestid', 'productid', 'qty', 'status'], + }, + { + name: 'products/getsaletemplate', + path: `${WEB}/products/getsaletemplate`, + params: { tenantid: probeTenant, locationid: probeLocation }, + needs: 'tenant', + expect: ['tenantid', 'locations', 'products'], + }, + { + name: 'customers/gettenantcustomers', + path: `${WEB}/customers/gettenantcustomers`, + params: { tenantid: probeTenant, locationid: probeLocation, pageno: 1, pagesize: 5 }, + needs: 'tenant', + expect: ['customerid', 'firstname', 'contactno'], + }, + { + name: 'orders/getorders', + path: `${WEB}/orders/tenant/getorders`, + params: { + tenantid: probeTenant, + locationid: probeLocation, + fromdate: isoDaysAgo(30), + todate: isoDaysAgo(0), + pageno: 1, + pagesize: 5, + }, + needs: 'tenant', + expect: ['orderheaderid', 'orderstatus'], + }, + { + name: 'deliveries/getdeliveries', + path: `${WEB}/deliveries/getdeliveries`, + params: { + tenantid: probeTenant, + locationid: probeLocation, + fromdate: isoDaysAgo(30), + todate: isoDaysAgo(0), + pageno: 1, + pagesize: 5, + }, + needs: 'tenant', + expect: ['orderheaderid', 'orderstatus'], + }, + { + name: 'pos/sales/summary', + path: `${POS}/sales/summary`, + params: { locationid: probeLocation, fromdate: isoDaysAgo(7), todate: isoDaysAgo(0) }, + needs: 'location', + expect: ['billcount', 'grosssales', 'taxcollected'], + }, + { + name: 'pos/sales', + path: `${POS}/sales`, + params: { locationid: probeLocation, pageno: 0, pagesize: 5 }, + needs: 'location', + expect: ['bills', 'total'], + }, + { + name: 'pos/health/location', + path: `${POS}/health/location`, + params: { location_id: probeLocation }, + needs: 'location', + expect: ['total', 'online', 'terminals'], + }, + { + name: 'tenants/getposusers', + path: `${WEB}/tenants/getposusers`, + params: { tenantid: probeTenant, locationid: probeLocation }, + needs: 'location', + expect: ['users', 'location_id'], + }, + { + name: 'tenants/getstaffs (MOB)', + path: `${MOB}/tenants/getstaffs`, + params: { tenantid: probeTenant }, + needs: 'tenant', + expect: ['userid', 'rolename', 'firstname'], + }, + { name: 'tenants/posroles', path: `${WEB}/tenants/posroles`, expect: ['role_id', 'role'] }, + { + name: 'tenants/getstaffshifts', + path: `${WEB}/tenants/getstaffshifts`, + params: { tenantid: probeTenant, locationid: probeLocation }, + needs: 'location', + expect: ['shifts', 'location_id'], + }, +]; + +function isoDaysAgo(days) { + const date = new Date(); + date.setDate(date.getDate() - days); + return date.toISOString().slice(0, 10); +} + +/* ── Run ─────────────────────────────────────────────────────────────────── */ + +let mismatches = 0; +let unreachable = 0; + +for (const check of CHECKS) { + if (check.needs === 'tenant' && !probeTenant) { + console.log(`SKIP ${check.name} — no tenant to probe with`); + continue; + } + if (check.needs === 'location' && !probeLocation) { + console.log(`SKIP ${check.name} — no location to probe with`); + continue; + } + + const { http, envelope } = await attempt(check.path, { params: check.params }); + const payload = envelope.details ?? envelope.data; + + const shape = Array.isArray(payload) + ? `array(${payload.length})` + : payload === null || payload === undefined + ? 'null' + : typeof payload; + + // The row to inspect: the first element of a list, or the object itself. + const row = Array.isArray(payload) ? payload[0] : payload; + const keys = row && typeof row === 'object' ? Object.keys(row) : []; + const missing = check.expect.filter((key) => !keys.includes(key)); + + const ok = envelope.status !== false && http < 400 && missing.length === 0; + if (!ok) mismatches += 1; + if (http >= 400 || envelope.status === false) unreachable += 1; + + console.log( + `${ok ? 'OK ' : 'CHECK'} ${check.name.padEnd(38)} http ${http} · code ${envelope.code ?? '—'} · ${shape}`, + ); + if (envelope.status === false || http >= 400) { + console.log(` message: ${envelope.message ?? '(none)'}`); + } + if (missing.length > 0 && keys.length > 0) { + console.log(` MISSING: ${missing.join(', ')}`); + console.log(` got: ${keys.sort().join(', ')}`); + } + if (keys.length === 0 && shape !== 'null' && !Array.isArray(payload)) { + console.log(` payload: ${JSON.stringify(payload).slice(0, 160)}`); + } +} + +console.log(''); +console.log(`${CHECKS.length} checked · ${mismatches} to look at · ${unreachable} refused`); +console.log( + mismatches === 0 + ? 'Every endpoint answered in the shape the console expects.' + : 'Anything marked CHECK either refused the call or is missing a key the console reads.', +); diff --git a/scripts/db.mjs b/scripts/db.mjs new file mode 100644 index 0000000..061a47e --- /dev/null +++ b/scripts/db.mjs @@ -0,0 +1,399 @@ +/** + * Direct database access, through Hasura. + * + * A scratchpad for reading and fixing rows that no screen exposes — setting a + * password on an account that was spawned without one, flipping a status, + * checking what the API is actually reading. It talks to the Hasura instance + * the old console proxies to (`api.workolik.com`), using the admin secret from + * `daily_merchant_web/.env`, which is gitignored and stays there. + * + * node scripts/db.mjs tables + * node scripts/db.mjs user care@nearle.in + * node scripts/db.mjs setpw care@nearle.in + * node scripts/db.mjs sql "select userid, authname from app_users limit 5" + * + * The secret is read from disk or the environment and never printed, never + * written anywhere, and never passed on the command line. + * + * ── Read this before using `setpw` ──────────────────────────────────────── + * This points at PRODUCTION. Every write here is immediate and unversioned. + * `setpw` refuses to run unless the account's password column is already + * empty, so it can only ever complete a setup that was never finished — it + * cannot overwrite a working login. Lift that guard only deliberately. + * + * Passwords in `app_users` are stored in clear. That is a property of this + * backend, not of this script; anything written here is readable by anyone + * with database access. + */ + +import { readFileSync, existsSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HERE = dirname(fileURLToPath(import.meta.url)); + +/** + * Where Hasura actually lives, discovered rather than assumed. + * + * The first version of this hardcoded `/v1/graphql` at the host root and got a + * 404. The old console's proxy is the clue it should have read: it rewrites + * `/hasura` to `/api/rest/`, which means Hasura is mounted under `/api`, not at + * the root. Rather than swap one guess for another, this tries the candidates + * and uses whichever answers. + * + * Override with HASURA_URL if it moves again — pass the full GraphQL URL. + */ +const ENDPOINT_CANDIDATES = process.env.HASURA_URL + ? [process.env.HASURA_URL] + : [ + 'https://api.workolik.com/api/v1/graphql', + 'https://api.workolik.com/v1/graphql', + 'https://api.workolik.com/hasura/v1/graphql', + ]; + +let ENDPOINT = ENDPOINT_CANDIDATES[0]; + +/** Finds the first candidate that answers a trivial query. */ +async function resolveEndpoint() { + for (const candidate of ENDPOINT_CANDIDATES) { + try { + const response = await fetch(candidate, { + method: 'POST', + headers: { 'content-type': 'application/json', 'x-hasura-admin-secret': SECRET }, + body: JSON.stringify({ query: '{ __typename }' }), + }); + if (!response.ok) continue; + const payload = await response.json().catch(() => null); + if (payload && !payload.errors) { + ENDPOINT = candidate; + return candidate; + } + } catch { + // Next candidate. + } + } + console.error( + 'Could not find the Hasura GraphQL endpoint. Tried:\n' + + ENDPOINT_CANDIDATES.map((c) => ` ${c}`).join('\n') + + '\nSet HASURA_URL to the full GraphQL URL and run again.', + ); + process.exit(1); +} + +/** Where the old console keeps its gitignored secret, relative to this repo. */ +const ENV_CANDIDATES = [ + resolve(HERE, '../../../nearle-daily/daily_merchant_web/.env'), + resolve(HERE, '../../daily_merchant_web/.env'), + 'D:/nearle-daily/daily_merchant_web/.env', +]; + +function readSecret() { + if (process.env.HASURA_ADMIN_SECRET) return process.env.HASURA_ADMIN_SECRET; + + for (const path of ENV_CANDIDATES) { + if (!existsSync(path)) continue; + const line = readFileSync(path, 'utf8') + .split(/\r?\n/) + .find((row) => row.startsWith('HASURA_ADMIN_SECRET=')); + if (!line) continue; + const value = line.slice('HASURA_ADMIN_SECRET='.length).trim().replace(/^["']|["']$/g, ''); + if (value) return value; + } + + console.error( + 'No admin secret found.\n' + + 'Expected HASURA_ADMIN_SECRET in one of:\n' + + ENV_CANDIDATES.map((p) => ` ${p}`).join('\n') + + '\nor set it in the environment for this command.', + ); + process.exit(1); +} + +const SECRET = readSecret(); + +async function gql(query, variables = {}) { + let response; + try { + response = await fetch(ENDPOINT, { + method: 'POST', + headers: { 'content-type': 'application/json', 'x-hasura-admin-secret': SECRET }, + body: JSON.stringify({ query, variables }), + }); + } catch (cause) { + console.error(`Could not reach ${ENDPOINT} — ${cause.message}`); + process.exit(1); + } + + const payload = await response.json().catch(() => null); + if (!payload) { + console.error(`Malformed response (HTTP ${response.status})`); + process.exit(1); + } + if (payload.errors) { + for (const error of payload.errors) console.error(`✗ ${error.message}`); + process.exit(1); + } + return payload.data; +} + +/* ── Commands ─────────────────────────────────────────────────────────────── */ + +/** Every table Hasura has tracked. Start here if a query says "field not found". */ +async function tables() { + const data = await gql(`{ __schema { queryType { fields { name } } } }`); + const names = data.__schema.queryType.fields + .map((field) => field.name) + .filter((name) => !name.endsWith('_aggregate') && !name.endsWith('_by_pk')) + .sort(); + console.log(names.join('\n')); + console.log(`\n${names.length} tables`); +} + +const USER_FIELDS = `userid authname firstname lastname contactno roleid status tenantid locationid configid`; + +async function findUser(email) { + const data = await gql( + `query ($email: String!) { + app_users(where: { authname: { _eq: $email } }) { ${USER_FIELDS} password } + }`, + { email }, + ); + return data.app_users ?? []; +} + +async function user(email) { + const rows = await findUser(email); + if (rows.length === 0) { + console.log(`No account with authname "${email}".`); + return; + } + for (const row of rows) { + // The password itself is never printed — only whether one exists, which is + // the only thing anyone needs to know from here. + const { password, ...rest } = row; + console.log({ ...rest, haspassword: String(password ?? '').trim() !== '' }); + } +} + +/** + * Completes a password setup that was never finished. + * + * Refuses if a password is already set. An account that can sign in must not + * be changeable from a scratchpad — that is a support action with a person + * behind it, not a one-liner. + */ +async function setpw(email, password) { + if (!password || password.length < 6) { + console.error('Password must be at least 6 characters (the backend enforces this too).'); + process.exit(1); + } + + const rows = await findUser(email); + if (rows.length === 0) { + console.error(`No account with authname "${email}".`); + process.exit(1); + } + if (rows.length > 1) { + console.error( + `${rows.length} accounts share that email (configid ${rows.map((r) => r.configid).join(', ')}).\n` + + 'Refusing to guess. Use `sql` with an explicit userid.', + ); + process.exit(1); + } + + const row = rows[0]; + if (String(row.password ?? '').trim() !== '') { + console.error( + `userid ${row.userid} already has a password. This command only completes an unfinished setup.\n` + + 'To reset a working login, do it deliberately with `sql`.', + ); + process.exit(1); + } + if (row.roleid === 7 || row.roleid === 8) { + console.error( + `userid ${row.userid} is a till account (roleid ${row.roleid}). Those sign in at the terminal with a PIN, not here.`, + ); + process.exit(1); + } + + const data = await gql( + `mutation ($userid: Int!, $password: String!) { + update_app_users(where: { userid: { _eq: $userid } }, _set: { password: $password }) { + affected_rows + } + }`, + { userid: row.userid, password }, + ); + + const affected = data.update_app_users?.affected_rows ?? 0; + if (affected !== 1) { + console.error(`Expected to update 1 row, updated ${affected}. Nothing assumed — check manually.`); + process.exit(1); + } + console.log( + `✓ Password set on userid ${row.userid} (${email}), roleid ${row.roleid}, tenantid ${row.tenantid}.`, + ); + console.log(' Sign in at the console with it now.'); +} + +/** + * Arbitrary read-only SQL, via Hasura's `run_sql`. + * + * Reads only. A statement that writes is refused here — writes go through a + * named command above, where they can carry their own guard. + */ +async function sql(statement) { + if (/^\s*(insert|update|delete|drop|alter|truncate|create)\b/i.test(statement)) { + console.error('This command runs reads only. Add a named command for a write.'); + process.exit(1); + } + + const endpoint = ENDPOINT.replace(/\/v1\/graphql$/, '/v2/query'); + const response = await fetch(endpoint, { + method: 'POST', + headers: { 'content-type': 'application/json', 'x-hasura-admin-secret': SECRET }, + body: JSON.stringify({ + type: 'run_sql', + args: { source: 'default', sql: statement, read_only: true }, + }), + }); + + const payload = await response.json().catch(() => null); + if (!response.ok || !payload) { + console.error(payload?.error ?? `HTTP ${response.status}`); + process.exit(1); + } + + const rows = payload.result ?? []; + for (const row of rows) console.log(row.join('\t')); + console.log(`\n${Math.max(0, rows.length - 1)} rows`); +} + +/** + * Why the app shows fewer products than the console does. + * + * node scripts/db.mjs appgap + * + * `getproductsbysubcategory` is what the customer app browses with, and three + * separate conditions decide whether a product survives it. None of them is an + * error and none of them is logged — a product that fails any one simply is not + * in the response, which is why the console can be full and the app empty. + * + * A. `WHERE a.categoryid = ?` — the caller passes 2, and the filter is not + * optional (`productRepository.go:865`). A product in any other category is + * invisible to this endpoint no matter what else is true of it. + * + * B. `WHERE pl.locationid = ?` on a LEFT JOIN to `productlocations`. A product + * with no row for THIS outlet joins to NULL, and the WHERE then drops it. + * Being in the catalogue is not the same as being on a shelf: something has + * to write `productlocations`, and nothing does that automatically. + * + * C. The grouping in `GetProductsBySubcategory` walks the real subcategories + * of category 2 and collects products matching each, then sweeps up + * everything with `subcategoryid = 0` as "Uncategorized". A product whose + * subcategoryid is non-zero but is NOT a subcategory of category 2 matches + * neither loop and vanishes — it is in the query results and absent from + * the response. This one is worth looking for first, because it looks like + * nothing at all. + */ +async function appgap(tenantid, locationid) { + const tid = Number(tenantid); + const lid = Number(locationid); + if (!tid || !lid) { + console.error('Usage: node scripts/db.mjs appgap '); + process.exit(1); + } + + // GraphQL, not `run_sql`. + // + // `run_sql` lives on Hasura's `/v2/query` admin API, which answered 404 here — + // it is disabled on managed instances and behind a different path on others. + // Three ordinary queries and the bucketing done in JS needs none of that, and + // works on any Hasura the admin secret can reach. + const data = await gql( + `query ($tid: Int!, $lid: Int!) { + products(where: { tenantid: { _eq: $tid } }) { + productid productname categoryid subcategoryid + } + productlocations(where: { tenantid: { _eq: $tid }, locationid: { _eq: $lid } }) { + productid + } + productsubcategories(where: { categoryid: { _eq: 2 } }) { + subcategoryid subcategoryname + } + }`, + { tid, lid }, + ); + + const products = data.products ?? []; + const listed = new Set((data.productlocations ?? []).map((row) => row.productid)); + const realSubs = new Map( + (data.productsubcategories ?? []).map((row) => [row.subcategoryid, row.subcategoryname]), + ); + + if (products.length === 0) { + console.log(`Tenant ${tid} has no products at all.`); + return; + } + + const buckets = new Map(); + const examples = new Map(); + for (const product of products) { + let reason; + if (product.categoryid !== 2) { + reason = 'A. categoryid is not 2 — the app only asks for category 2'; + } else if (!listed.has(product.productid)) { + reason = 'B. not listed at this outlet — no productlocations row'; + } else if (product.subcategoryid !== 0 && !realSubs.has(product.subcategoryid)) { + reason = 'C. subcategoryid is not a real subcategory of 2 — silently dropped'; + } else { + reason = 'OK. should appear in the app'; + } + buckets.set(reason, (buckets.get(reason) ?? 0) + 1); + if (!examples.has(reason)) examples.set(reason, product); + } + + console.log(`${products.length} products on tenant ${tid}\n`); + const ordered = [...buckets.entries()].sort((a, b) => b[1] - a[1]); + for (const [reason, count] of ordered) { + const sample = examples.get(reason); + console.log(` ${String(count).padStart(5)} ${reason}`); + console.log( + ` e.g. ${sample.productname} (id ${sample.productid}, category ${sample.categoryid}, subcategory ${sample.subcategoryid})`, + ); + } + + console.log(`\nReal subcategories of category 2: ${[...realSubs.values()].join(', ') || '(none)'}`); +} + +/* ── Dispatch ─────────────────────────────────────────────────────────────── */ + +const [command, ...rest] = process.argv.slice(2); + +const COMMANDS = { + tables: () => tables(), + user: () => user(rest[0]), + setpw: () => setpw(rest[0], rest[1]), + sql: () => sql(rest.join(' ')), + appgap: () => appgap(rest[0], rest[1]), +}; + +if (!command || !COMMANDS[command]) { + console.log( + [ + 'node scripts/db.mjs ', + '', + ' tables every table Hasura has tracked', + ' user show an account (never prints the password)', + ' setpw set a password on an account that has none', + ' sql " onChange(event.target.value)} + /> + {/* Only when there is something to clear. A permanently visible X on an + empty field is a control that does nothing, and it costs the + placeholder ten characters of room. */} + {value ? ( + + ) : null} + + ); +} diff --git a/src/components/SectionHeader.tsx b/src/components/SectionHeader.tsx new file mode 100644 index 0000000..d5f18e6 --- /dev/null +++ b/src/components/SectionHeader.tsx @@ -0,0 +1,39 @@ +import type { ReactNode } from 'react'; +import { Heading } from '@astryxdesign/core/Heading'; +import { Text } from '@astryxdesign/core/Text'; +import { HStack } from '@astryxdesign/core/HStack'; + +export interface SectionHeaderProps { + title: string; + /** + * Sits inline beside the title, not beneath it — and only when it carries a + * fact, such as a count. + * + * It used to be required, on the theory that every section should say what it + * is showing. In practice that produced a line of editorial beside every + * heading — "how today is going", "a queue nobody can see from the shop + * floor" — which reads as noise once you have seen the page twice. The + * heading and the data under it say enough. + */ + note?: string; + /** A control or a "View all" link, right-aligned. */ + action?: ReactNode; +} + +export function SectionHeader({ title, note, action }: SectionHeaderProps) { + return ( + + + + {title} + + {note ? ( + + {note} + + ) : null} + + {action} + + ); +} diff --git a/src/components/SheetDropzone.tsx b/src/components/SheetDropzone.tsx new file mode 100644 index 0000000..0e69d19 --- /dev/null +++ b/src/components/SheetDropzone.tsx @@ -0,0 +1,235 @@ +import { useRef, useState, type DragEvent } from 'react'; +import { FileSpreadsheet, Trash2, Upload } from 'lucide-react'; + +/** + * The workbook dropzone. + * + * Astryx's `FileInput` in dropzone mode is a white slab with a dashed rule and + * a bare arrow — correct, and completely mute about what it wants or whether it + * is even available. This one says all three things: what it takes, whether it + * can take it right now, and what it is holding. + * + * Three states, and each looks different at a glance rather than on reading: + * + * - **Waiting** — a tinted panel with a brand-lit icon and an explicit + * "Choose file" affordance, because a dropzone that only accepts a drag is + * unusable to anyone on a laptop with the file already in a dialog. + * - **Dragging** — the brand colour comes up and the panel lifts. A dropzone + * that does not visibly react is one people drop next to. + * - **Filled** — the file itself, with its size and a way to swap it. The old + * control kept saying "Drop an .xlsx here" after you already had. + * + * It stays a real `` under the surface, so the keyboard, the + * screen reader and the OS file dialog all behave as they should — the div is + * decoration over an input, not a replacement for one. + */ +export interface SheetDropzoneProps { + file: File | null; + onFile: (file: File | null) => void; + /** Comma-separated extensions, passed straight to the input. */ + accept?: string; + /** Why the control is unavailable. Present means disabled. */ + blockedReason?: string; +} + +const ACCEPTED = ['.xlsx', '.xls', '.csv']; + +export function SheetDropzone({ + file, + onFile, + accept = ACCEPTED.join(','), + blockedReason, +}: SheetDropzoneProps) { + const inputRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const isDisabled = Boolean(blockedReason); + + function open() { + if (!isDisabled) inputRef.current?.click(); + } + + function handleDrop(event: DragEvent) { + event.preventDefault(); + setIsDragging(false); + if (isDisabled) return; + const dropped = event.dataTransfer.files?.[0]; + if (dropped) onFile(dropped); + } + + /* ── Filled ───────────────────────────────────────────────────────────── */ + + if (file) { + return ( +

+ + + + +
+
+ {file.name} +
+
+ {formatSize(file.size)} +
+
+ + + + + onFile(event.target.files?.[0] ?? null)} + style={{ display: 'none' }} + /> +
+ ); + } + + /* ── Waiting / dragging ───────────────────────────────────────────────── */ + + return ( +
{ + event.preventDefault(); + if (!isDisabled) setIsDragging(true); + }} + onDragLeave={() => setIsDragging(false)} + onDrop={handleDrop} + role="button" + tabIndex={isDisabled ? -1 : 0} + aria-disabled={isDisabled} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + open(); + } + }} + style={{ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: 8, + padding: '16px 20px', + borderRadius: 'var(--card-radius)', + border: `1.5px dashed ${ + isDragging ? 'var(--color-brand)' : 'color-mix(in oklab, var(--color-brand) 26%, var(--color-line))' + }`, + background: isDragging + ? 'color-mix(in oklab, var(--color-brand) 8%, transparent)' + : 'color-mix(in oklab, var(--color-brand) 3%, transparent)', + cursor: isDisabled ? 'not-allowed' : 'pointer', + opacity: isDisabled ? 0.55 : 1, + textAlign: 'center', + outline: 'none', + transition: 'background .16s ease, border-color .16s ease, transform .16s ease', + transform: isDragging ? 'scale(1.004)' : 'none', + }} + > + + + + +
+
+ {isDisabled + ? blockedReason + : isDragging + ? 'Drop it here' + : 'Drop your workbook, or click to browse'} +
+
+ {ACCEPTED.join(' · ')} +
+
+ + onFile(event.target.files?.[0] ?? null)} + style={{ display: 'none' }} + /> +
+ ); +} + +const ghostButtonStyle: React.CSSProperties = { + flex: 'none', + display: 'inline-flex', + alignItems: 'center', + gap: 6, + padding: '7px 12px', + borderRadius: 9, + border: '1px solid var(--color-line)', + background: 'var(--color-surface)', + color: 'var(--color-ink-2)', + fontSize: 12.5, + fontWeight: 600, + fontFamily: 'inherit', + cursor: 'pointer', +}; + +/** kB up to a megabyte, then MB. Nobody needs "1420800 bytes". */ +function formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + const kb = bytes / 1024; + if (kb < 1024) return `${Math.round(kb)} kB`; + return `${(kb / 1024).toFixed(1)} MB`; +} diff --git a/src/components/StickyRow.tsx b/src/components/StickyRow.tsx new file mode 100644 index 0000000..392b723 --- /dev/null +++ b/src/components/StickyRow.tsx @@ -0,0 +1,59 @@ +import { useEffect, useRef, useState, type ReactNode } from 'react'; + +/** + * A row of page controls held under the nav bar while the page scrolls. + * + * Whatever sits above a page — its view tabs, its search, its action buttons — + * used to leave with the scroll. Reaching the bottom of a long list put every + * control over that list off the top of the window, and the only way back to + * them was to scroll back through everything just read. The further in somebody + * was, the further from the controls. + * + * The page still scrolls normally. Nothing here gets a scrollbar of its own — + * the row simply stays where it is. Positioning and the canvas it paints live + * in `.page-sticky`; this component exists for the one part CSS cannot answer, + * which is whether the row is currently pinned. + * + * `PageHeader` wraps itself in this, so every page built from it gets the + * behaviour without asking. Dispatch builds its own switcher rather than going + * through `PageHeader`, so it wraps itself — which is the reason this is a + * component and not four more lines inside the header. + */ +export function StickyRow({ children }: { children: ReactNode }) { + /** + * Pinned or not, so the hairline can be drawn only when it is. + * + * At rest the nav bar's own hairline sits just above this one, and two + * parallel lines across the page with nothing between them read as a mistake. + * The line is what separates the row from content passing UNDER it, so it + * belongs to the pinned state rather than to the row. + * + * Watched with an observer rather than a scroll handler: a scroll listener + * runs on every frame of every scroll to answer a question that changes + * twice. The negative top margin puts the observer's boundary a pixel above + * where the row comes to rest, so "no longer fully inside that box" means + * exactly "stuck". + */ + const rowRef = useRef(null); + const [isStuck, setIsStuck] = useState(false); + + useEffect(() => { + const element = rowRef.current; + /* Guarded for any renderer without it — the row still sticks, it just + never grows its line, which is a missing hairline rather than a crash. */ + if (!element || typeof IntersectionObserver === 'undefined') return; + + const observer = new IntersectionObserver( + ([entry]) => setIsStuck(Boolean(entry && !entry.isIntersecting)), + { rootMargin: '-57px 0px 0px 0px', threshold: 1 }, + ); + observer.observe(element); + return () => observer.disconnect(); + }, []); + + return ( +
+ {children} +
+ ); +} diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx new file mode 100644 index 0000000..d112e59 --- /dev/null +++ b/src/components/TabBar.tsx @@ -0,0 +1,100 @@ +import type { ReactNode } from 'react'; +import './tabBar.css'; + +/** + * The console's view switcher — Orders / Deliveries / Counter sales, Catalogue + * / My stock / My requests, Store users / Terminal accounts / Riders. + * + * ── There were six of these ───────────────────────────────────────────────── + * + * `TabButton` was defined privately in `StoresPage`, `InventoryPage`, + * `ReportsPage`, `SalesPage`, `UsersPage` and `store-user/ui.tsx` — six copies + * of the same button, each styled inline, and they had already drifted: three + * of them filled the count badge with `--color-warning`, so "85" under Orders + * was drawn in the colour this console reserves for something being wrong. A + * count is not an alarm. Sales had the sensible version. Nobody could see the + * difference because no two of them are ever on screen together. + * + * ── Why it is a track and not loose pills ─────────────────────────────────── + * + * The old buttons were transparent until selected, so an unselected set read as + * four pieces of text floating above the page with no indication they were one + * control or that picking one deselects the others. They sit in a recessed + * track now, with the selected tab raised out of it on the card surface — the + * standard segmented control, and the one shape that says "these are the views, + * you are in this one" without needing a label to say so. + */ + +export interface TabBarProps { + children: ReactNode; + /** `sm` for a secondary strip — a filter under a primary switcher. */ + size?: 'md' | 'sm'; + 'aria-label'?: string; +} + +export function TabBar({ children, size = 'md', ...rest }: TabBarProps) { + return ( +
+ {children} +
+ ); +} + +export interface TabProps { + label: string; + icon?: ReactNode; + isActive: boolean; + onClick: () => void; + /** + * The count beside the label. Omit it and no count is drawn. + * + * Whether ZERO is worth drawing depends on what the tab is, so the caller + * decides rather than this component: + * + * - A VIEW passes `x.length || undefined`. "Deliveries 0" beside "Orders + * 85" reads as a broken tab; "Deliveries" alone says nothing and lets the + * empty state explain. + * - A STATUS in a ladder passes the raw count. "Cancelled 0" is the answer + * to a question the reader is asking — nothing was cancelled — and a rung + * that loses its number when it empties makes the ladder jump about as + * the day goes on. + */ + count?: number | undefined; + /** + * Draws a hairline before this tab, dividing the track into groups. + * + * Sales needs it: Orders and Deliveries are two halves of the app's own + * trade, and Counter sales is a different channel entirely. The rule says so + * without spending a tab on a heading. + */ + isGroupStart?: boolean; + /** + * A pulsing dot after the label, for a view that updates by itself. + * + * Dispatch's Active Fleet is the one: it reads rider positions live, and the + * dot is how the board says so. A named prop rather than a general-purpose + * slot — a `trailing` that took any node would be the seam this component + * exists to close, and there is exactly one live view in the console. + */ + isLive?: boolean; +} + +export function Tab({ label, icon, isActive, onClick, count, isGroupStart, isLive }: TabProps) { + return ( + + ); +} diff --git a/src/components/TablePager.tsx b/src/components/TablePager.tsx new file mode 100644 index 0000000..7d723ce --- /dev/null +++ b/src/components/TablePager.tsx @@ -0,0 +1,49 @@ +import { Pagination } from '@astryxdesign/core/Pagination'; +import type { Paged } from './usePaged'; +import { PAGE_SIZES } from './usePaged'; +import './tablePager.css'; + +/** + * The pager that sits under a table. + * + * One component so every table in the console counts, labels and behaves the + * same way, rather than each page inventing its own row of buttons. + * + * ── It hides itself ───────────────────────────────────────────────────────── + * + * Nothing renders while everything fits on one page. That is what makes it safe + * to put under EVERY table, including the ones that usually hold four rows: a + * pager reading "1–4 of 4" next to a dead prev/next pair is noise, and noise + * under every table is worse than no pager at all. It appears exactly when it + * has something to offer. + * + * `variant="count"` — "21–40 of 96" — rather than a strip of page numbers. + * With 25 rows a page a busy day is four pages, and the number an operator + * actually wants is how much is left, not which of four buttons is lit. + */ +export function TablePager({ + paged, + label = 'rows', +}: { + paged: Paged; + /** What is being counted, for the screen-reader label: "orders", "products". */ + label?: string; +}) { + if (paged.totalPages <= 1) return null; + + return ( +
+ +
+ ); +} diff --git a/src/components/TrailMap.tsx b/src/components/TrailMap.tsx new file mode 100644 index 0000000..fc41334 --- /dev/null +++ b/src/components/TrailMap.tsx @@ -0,0 +1,436 @@ +import { useEffect, useRef } from 'react'; +import L from 'leaflet'; +import 'leaflet/dist/leaflet.css'; +import './trailMap.css'; + +/** + * A map, for things that have a position. + * + * ── Why leaflet directly and not react-leaflet ────────────────────────────── + * + * A leaflet map is an imperative object that owns a DOM node and must be torn + * down by hand — `remove()`, or the tile layer keeps fetching and the container + * keeps its `_leaflet_id` and refuses to be reused. React-leaflet wraps that in + * components and adds a second package that has to track React's major version + * forever. The wrapping is about forty lines; it is written here instead. + * + * ── Why the pins are divIcons ─────────────────────────────────────────────── + * + * Leaflet's default marker is a PNG resolved relative to the stylesheet, which + * every bundler rewrites and breaks — the classic "markers are invisible" + * bug, usually patched by re-pointing the icon URLs at a CDN. A `divIcon` is + * markup, so it ships with the bundle, takes the console's brand colour and + * needs no image at all. + */ + +export interface MapTrail { + id: string | number; + label: string; + points: readonly { lat: number; lng: number }[]; + colour: string; + /** + * Drawn thinner and paler, so a focused round reads against the others. + * + * Muted rather than hidden: a rider's route is still context for the rider + * beside them, and hiding it would make two overlapping rounds impossible to + * compare — which is the reason to focus one in the first place. + */ + isMuted?: boolean; +} + +export interface MapPinPopup { + title?: string; + orderId?: string; + customer?: string; + address?: string; + status?: string; + statusColor?: string; + time?: string; + rider?: string; + amount?: string; + step?: number; + subtitle?: string; + items?: { orderid: string; status: string }[]; +} + +export interface MapPin { + id: string | number; + lat: number; + lng: number; + label: string; + /** Shown under the label in the popup. Plain text, one line per entry. */ + lines?: string[]; + popup?: MapPinPopup; + popupHtml?: string; + colour?: string; + /** A hollow ring rather than a filled pin — for a last-known, not-live point. */ + isFaded?: boolean; + /** + * A square marks a place rather than an event — a shop, a depot. Round pins + * are the things that happened there, so a branch never reads as one more + * customer among its own drops. + */ + shape?: 'round' | 'square'; + /** + * The stop's place in the round, drawn inside the pin. + * + * Without it a route is a line through identical dots and there is no way to + * read which end it started from — the map says "this is the order it was + * worked" and then withholds the order. The console this replaces numbers + * every drop for the same reason. + */ + step?: number; + /** Ringed as the round's first stop. */ + isStart?: boolean; + /** + * Dim this pin. Used when another round is focused, so the one being read + * stands out without the rest disappearing — a rider's stops are still + * context for the rider beside them. + */ + isMuted?: boolean; +} + +/** Tamil Nadu, so an empty map still shows the right part of the world. */ +const FALLBACK: L.LatLngExpression = [11.0168, 76.9558]; + +export function TrailMap({ + trails = [], + pins = [], + height = 380, + emptyNote = 'Nothing to place on the map yet.', +}: { + trails?: readonly MapTrail[]; + pins?: readonly MapPin[]; + height?: number; + emptyNote?: string; +}) { + const host = useRef(null); + const map = useRef(null); + /* Everything drawn, kept together so a redraw clears exactly what it drew. + Clearing the map wholesale would take the tile layer with it. */ + const drawn = useRef(null); + + const isEmpty = trails.every((trail) => trail.points.length === 0) && pins.length === 0; + + useEffect(() => { + if (!host.current || map.current) return; + const instance = L.map(host.current, { + center: FALLBACK, + zoom: 12, + // The console scrolls; a wheel over the map should scroll the page, not + // zoom. Ctrl+wheel and the +/− buttons still zoom, which is what people + // expect from a map embedded in a document. + scrollWheelZoom: false, + attributionControl: true, + }); + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19, + attribution: '© OpenStreetMap contributors', + }).addTo(instance); + drawn.current = L.layerGroup().addTo(instance); + map.current = instance; + + return () => { + instance.remove(); + map.current = null; + drawn.current = null; + }; + }, []); + + useEffect(() => { + const instance = map.current; + const layer = drawn.current; + if (!instance || !layer) return; + + layer.clearLayers(); + const bounds = L.latLngBounds([]); + + for (const trail of trails) { + if (trail.points.length < 2) continue; + const line = trail.points.map((point) => [point.lat, point.lng] as [number, number]); + + /* A white casing under the coloured line. + Map tiles are busy and mid-toned, and a 3px stroke of any colour + disappears into a main road drawn underneath it. The casing gives the + line its own edge so it reads as something laid ON the map rather than + part of it. Borrowed from the console this replaces, which draws the + same pair — it is the one thing that made its routes legible. */ + L.polyline(line, { + color: '#ffffff', + weight: 7, + // Muted with its line, or a dimmed route shows a bright white casing + // and ends up louder than the one being focused. + opacity: trail.isMuted ? 0.2 : 0.65, + lineJoin: 'round', + lineCap: 'round', + interactive: false, + }).addTo(layer); + + L.polyline(line, { + color: trail.colour, + weight: trail.isMuted ? 2.5 : 3.5, + opacity: trail.isMuted ? 0.35 : 0.95, + // Rounded joins, or a dense trail draws spikes at every turn. + lineJoin: 'round', + lineCap: 'round', + }) + .bindTooltip(trail.label, { sticky: true }) + .addTo(layer); + + line.forEach((point) => bounds.extend(point)); + } + + for (const pin of pins) { + const colour = pin.colour ?? 'var(--color-brand)'; + L.marker([pin.lat, pin.lng], { + title: pin.label, + icon: L.divIcon({ + className: 'trail-pin-wrap', + // A numbered pin has to be big enough to hold two digits legibly, so + // the size follows the content rather than being fixed. + html: + `` + + `${pin.step ? escapeHtml(String(pin.step)) : ''}`, + iconSize: pin.step ? [22, 22] : [16, 16], + iconAnchor: pin.step ? [11, 11] : [8, 8], + }), + }) + .bindPopup(pin.popupHtml ?? formatPinPopup(pin), { + className: 'trail-leaflet-popup-card', + closeButton: true, + maxWidth: 290, + minWidth: 230, + autoPanPadding: [20, 20], + }) + .addTo(layer); + bounds.extend([pin.lat, pin.lng]); + } + + if (bounds.isValid()) { + // `maxZoom` matters: a single pin, or a rider who never left one street, + // otherwise zooms to building level and the map shows one grey rectangle + // with no landmarks to orient by. + instance.fitBounds(bounds, { padding: [28, 28], maxZoom: 16 }); + } + }, [trails, pins]); + + /* Leaflet measures its container once, at construction. Inside a drawer or a + tab the container is often zero-height at that moment, and the map renders + as a grey strip with one tile in the corner until something resizes the + window. Re-measuring whenever the box changes size fixes it for good, + including when the drawer animates open. */ + useEffect(() => { + const node = host.current; + const instance = map.current; + if (!node || !instance || typeof ResizeObserver === 'undefined') return; + const observer = new ResizeObserver(() => instance.invalidateSize()); + observer.observe(node); + return () => observer.disconnect(); + }, []); + + return ( +
+
+ {isEmpty ?
{emptyNote}
: null} +
+ ); +} + +/** + * Distinct, legible line colours. + * + * Hand-picked rather than generated from a hue wheel: evenly spaced hues put + * two yellows next to each other on an OSM tile and both vanish. These are all + * dark enough to read over map detail and different enough to tell apart at the + * width of a polyline. + */ +export const TRAIL_COLOURS = [ + '#662582', + '#0f8a5f', + '#c2410c', + '#1d4ed8', + '#b91c1c', + '#0e7490', + '#7c2d12', + '#4d7c0f', +] as const; + +export function trailColour(index: number): string { + return TRAIL_COLOURS[index % TRAIL_COLOURS.length] as string; +} + +function formatPinPopup(pin: MapPin): string { + const p = pin.popup; + if (p) { + if (p.items && p.items.length > 1) { + return ` +
+
+
+ 📦 + ${p.items.length} Orders Here +
+ ${p.step ? `Stop #${escapeHtml(String(p.step))}` : ''} +
+
+
+
🏢
+
+ ${escapeHtml(p.customer || 'Multiple Drops')} + ${escapeHtml(p.address || '')} +
+
+
+ ${p.items.slice(0, 4).map(item => ` +
+ #${escapeHtml(item.orderid)} + ${escapeHtml(item.status)} +
+ `).join('')} + ${p.items.length > 4 ? `
+${p.items.length - 4} more orders
` : ''} +
+
+ +
+ `; + } + + if (p.orderId) { + return ` +
+
+
+ 📦 + #${escapeHtml(p.orderId)} +
+ ${p.step ? `Stop #${escapeHtml(String(p.step))}` : ''} +
+ +
+
+
${escapeHtml((p.customer || 'C').charAt(0).toUpperCase())}
+
+ ${escapeHtml(p.customer || 'Customer')} + ${escapeHtml(p.address || 'Delivery Address')} +
+
+ +
+ + + ${escapeHtml(p.status || 'Pending')} + ${p.time ? `· ${escapeHtml(p.time)}` : ''} + +
+
+ + +
+ `; + } + + const isStoreHub = p.subtitle?.includes('Hub') || p.title?.includes('Hub') || p.title?.includes('R mart') || pin.shape === 'square'; + + if (isStoreHub) { + return ` +
+
+
+ 🏪 + Store Hub +
+ Origin +
+ +
+
+
🏪
+
+ ${escapeHtml(p.title || 'R mart')} + ${escapeHtml(p.subtitle || 'Fulfillment & Dispatch Hub')} +
+
+ +
+ 📍 + ${escapeHtml(p.address || 'RS Puram Main Rd, D.B. Road, Coimbatore - 641002')} +
+ +
+ + + ${escapeHtml(p.status || 'Active Origin Depot')} + +
+
+
+ `; + } + + return ` +
+
+
+ 🛵 + ${escapeHtml(p.title || pin.label)} +
+
+
+ ${p.subtitle ? `
${escapeHtml(p.subtitle)}
` : ''} + ${p.address ? `
${escapeHtml(p.address)}
` : ''} + ${p.status ? ` +
+ + + ${escapeHtml(p.status)} + ${p.time ? `· ${escapeHtml(p.time)}` : ''} + +
+ ` : ''} +
+
+ `; + } + + // Fallback for default pins + return ` +
+
+ ${escapeHtml(pin.label)} +
+
+ ${(pin.lines ?? []).map(line => `
${escapeHtml(line)}
`).join('')} +
+
+ `; +} + +function escapeHtml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +/** Popup and icon HTML is a string, so a rider named `` must not become markup. */ +function escapeAttr(value: string): string { + return value.replace(/["'<>]/g, ''); +} diff --git a/src/components/kpiCard.css b/src/components/kpiCard.css new file mode 100644 index 0000000..9cd2a0b --- /dev/null +++ b/src/components/kpiCard.css @@ -0,0 +1,140 @@ +/* ══ The KPI tile ══════════════════════════════════════════════════════════ + The console's headline figure, and the card design the rest of the site + follows: the same surface tokens, the same icon tile, the same + label / value / note hierarchy. + + Layout is a stack: a top row carrying the label and the icon tile, then the + figure at full card width, then the note. + + The figure is LEFT-aligned and owns the whole width, and both halves of that + matter. The old tile centred it, so the number sat in a different place in + every card depending on how many digits it had and a row of tiles never + lined up. The tile before this one put the icon in a column to the left, + which left about 134px for the figure at the width these actually get — and + "₹1,24,500" wrapped mid-number. */ + +.kpi-card { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + /* A floor, not a height. Labels wrap to two lines at narrow widths — with + Nearle Buddy open these are about 195px wide, and "Cancelled orders" + ellipsised to "Cance…" is a label that has stopped working — so the tile + grows and the row stays level. */ + min-height: 104px; + padding: 16px; + + /* The console's one card surface. Same tokens as `Panel` and every other + card in the app; see the note in `index.css`. */ + border: var(--card-border); + border-radius: var(--card-radius); + background: var(--card-bg); + box-shadow: var(--card-shadow); +} + +/* ── The icon tile ────────────────────────────────────────────────────────── + A rounded square in the tone's tint, one step tighter than the card that + holds it — a control radius inside a card radius, which is what keeps it + from reading as a small card of its own. */ +.kpi-card-icon { + width: 40px; + height: 40px; + flex: none; + display: grid; + place-items: center; + border-radius: var(--card-radius-sm); + background: var(--kpi-tint); + color: var(--kpi-ink); +} + +/* Brand by default — a tile that is merely reporting should not shout. The + other three are for tiles whose figure IS a state. */ +.kpi-card { + --kpi-ink: var(--color-brand); + --kpi-tint: var(--color-brand-tint); +} +.kpi-card[data-tone='success'] { + --kpi-ink: var(--color-success, #1f9d55); + --kpi-tint: color-mix(in oklab, var(--color-success, #1f9d55) 12%, transparent); +} +.kpi-card[data-tone='warning'] { + --kpi-ink: var(--color-warning, #b7860b); + --kpi-tint: color-mix(in oklab, var(--color-warning, #b7860b) 14%, transparent); +} +.kpi-card[data-tone='error'] { + --kpi-ink: var(--color-error, #d64545); + --kpi-tint: color-mix(in oklab, var(--color-error, #d64545) 12%, transparent); +} + +/* ── The top row ────────────────────────────────────────────────────────── */ +/* Label left, icon right. The label takes the space the icon does not, and the + figure below gets the whole card. */ +.kpi-card-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; + min-width: 0; + margin-bottom: 8px; +} + +/* The label names the figure and stays quiet doing it. Not caps: at 11px in + caps with letter-spacing, "Cancelled orders" runs wider than the tile and a + two-line eyebrow reads as a heading that broke. Sentence case, ink-3. */ +.kpi-card-label { + font: 500 12.5px/1.3 var(--font-sans); + color: var(--color-ink-3); + min-width: 0; + /* Two lines, then ellipsis — never a third. */ + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* The figure. Tabular so a column of tiles lines up on the digits, and pulled + in very slightly — at 25px the default tracking makes a rupee figure look + spaced out rather than set. + + IT NEVER WRAPS. `overflow-wrap: anywhere` was on this rule and it broke + "₹1,24,500" across two lines between the 0 and the 0 — a figure split + mid-digit is not a smaller figure, it is a wrong one. A number too wide for + its tile ellipsises instead, which is visibly truncated rather than quietly + misread, and the `title` is not needed because the same figure is on the + page it links to. */ +.kpi-card-value { + font: 700 25px/1.2 var(--font-sans); + letter-spacing: -0.02em; + color: var(--color-ink-1); + font-variant-numeric: tabular-nums; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* What the figure is made of. This is the line that makes the number mean + something — see the note in `KpiCard.tsx` for why it had to come back. */ +.kpi-card-note { + margin-top: 3px; + font: 400 11.5px/1.4 var(--font-sans); + color: var(--color-ink-4); + /* Two lines at most. "142 app · 312 counter" wraps at tile width and that is + fine; a third line would push the tile past its neighbours. */ + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* `.kpi-grid` is NOT defined here. It already exists in `index.css`, built on + container queries rather than viewport ones — which is what lets a tile row + reflow when Nearle Buddy opens beside it and takes 340px out of the column, + something a viewport breakpoint cannot see. Defining a second one here would + have won on import order and silently thrown that away. */ + +@media (max-width: 600px) { + .kpi-card { min-height: 96px; padding: 14px; } + .kpi-card-icon { width: 32px; height: 32px; } + .kpi-card-value { font-size: 22px; } +} diff --git a/src/components/panel.css b/src/components/panel.css new file mode 100644 index 0000000..fffba0f --- /dev/null +++ b/src/components/panel.css @@ -0,0 +1,141 @@ +/* ══ The panel — the console's one card ════════════════════════════════════ + Three zones: a header band that says what this is and how much of it there + is, a body, and a footer band that says where you are in it. See the note at + the top of `Panel.tsx` for why the card grew an anatomy rather than a nicer + outline. + + Every size here comes from the card tokens in `index.css`. */ + +.panel-card { + display: flex; + flex-direction: column; + min-width: 0; + border: var(--card-border); + border-radius: var(--card-radius); + background: var(--card-bg); + box-shadow: var(--card-shadow); + /* The clip belongs on the element that owns the radius, or the header band's + tint squares off the top two corners. */ + overflow: hidden; +} + +/* For a grid row of panels that should finish level with each other. */ +.panel-card[data-filled='true'] { height: 100%; } + +/* ── Header band ────────────────────────────────────────────────────────── */ +/* Tinted rather than white, so the band reads as chrome and the rows below it + read as data. A white header separated only by a rule is the same surface + twice, which is how a heading ends up looking like a first table row. */ +.panel-card-head { + flex: none; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + min-height: 48px; + padding: 8px 16px; + background: var(--color-surface-subtle); + border-bottom: var(--card-border); +} + +.panel-card-title-group { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +/* An EYEBROW, not a heading. 11px caps: this labels the panel, and a 20px + title inside a card competes with the page's own title above it — the two + stacked is what made every screen say its subject twice. */ +.panel-card-title { + margin: 0; + font: 600 11px/1.2 var(--font-sans); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-ink-3); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* The count, in its own chip. Tabular, because it changes as a filter narrows + and a figure that shifts sideways while you read it is hard to trust. */ +.panel-card-count { + display: inline-flex; + align-items: baseline; + gap: 4px; + flex: none; + padding: 2px 8px; + border-radius: 999px; + background: var(--color-surface-sunken); + font: 600 11.5px/1.45 var(--font-sans); + font-variant-numeric: tabular-nums; + color: var(--color-ink-2); +} +.panel-card-count-label { + font-weight: 400; + font-size: 11px; + color: var(--color-ink-4); +} + +.panel-card-actions { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-left: auto; +} + +/* ── Body ───────────────────────────────────────────────────────────────── */ + +.panel-card-body { + flex: 1 1 auto; + min-width: 0; + min-height: 0; +} +.panel-card-body[data-pad='md'] { padding: 16px; } + +/* ── Footer band ────────────────────────────────────────────────────────── */ + +.panel-card-foot { + flex: none; + padding: 8px 16px; + background: var(--color-surface-subtle); + border-top: var(--card-border); +} + +/* The pager hides itself on a single page — it returns null — but the band + around it is rendered by the parent, which cannot see that. Without this a + short table carried a 44px strip of tinted chrome saying nothing. + `:has` collapses the band when its child rendered no DOM at all. */ +.panel-card-foot:not(:has(*)) { + display: none; +} + +/* The pager's own wrapper adds padding meant for sitting loose under a table. + Inside the band the band supplies it. */ +.panel-card-foot .table-pager { + padding: 0; + border-top: 0; +} + +/* ── A table sitting flush in the body ────────────────────────────────────── + The table brings its own horizontal scroll and its own header row. What it + must not bring is a rounded corner or an outer border — the panel owns both, + and a table with its own edge inside a panel is the double line this design + is meant to remove. */ +.panel-card-body > .table-scroll { + border-radius: 0; + border: 0; +} + +/* ── Narrow ─────────────────────────────────────────────────────────────── */ +@media (max-width: 600px) { + .panel-card-head { padding: 8px 12px; } + .panel-card-foot { padding: 8px 12px; } + .panel-card-body[data-pad='md'] { padding: 12px; } + /* Actions drop to their own line rather than squeezing the title out. */ + .panel-card-actions { margin-left: 0; width: 100%; } +} diff --git a/src/components/searchInput.css b/src/components/searchInput.css new file mode 100644 index 0000000..14bd460 --- /dev/null +++ b/src/components/searchInput.css @@ -0,0 +1,82 @@ +/* ══ Search box ════════════════════════════════════════════════════════════ + Shaped to match the view-tab track it sits opposite — see the note in + `SearchInput.tsx`. Every number here is the same number `tabBar.css` uses. */ + +.searchbox { + display: inline-flex; + align-items: center; + gap: 7px; + /* The track's height and the track's corner. This is the whole point of the + component: a search that is visibly the same object as the buttons at the + other end of the row. */ + height: 38px; + padding: 0 10px; + border-radius: var(--card-radius-sm); + border: var(--card-border); + background: var(--card-bg); + box-shadow: var(--card-shadow); + max-width: 100%; + transition: border-color 160ms ease, box-shadow 160ms ease; +} + +/* Fills its container, for a box sitting directly above the list it filters. */ +.searchbox[data-full='true'] { display: flex; width: 100%; } + +.searchbox:hover { border-color: var(--color-ink-4); } + +/* Focus lands on the BOX, not on the bare input inside it — the input has no + border of its own, so the default ring would draw inside the box and read as + a second, smaller field. */ +.searchbox:focus-within { + border-color: var(--color-brand); + box-shadow: 0 0 0 3px var(--color-brand-tint); +} + +.searchbox-icon { flex: none; color: var(--color-ink-4); } +.searchbox:focus-within .searchbox-icon { color: var(--color-brand); } + +.searchbox-input { + flex: 1 1 auto; + min-width: 0; + border: 0; + padding: 0; + background: transparent; + color: var(--color-ink-1); + font: 400 13px/1 var(--font-sans); + outline: none; + /* Safari draws its own rounded field and inner shadow on a text input, and + `type="search"` adds a magnifier and a cancel button on top of ours. */ + appearance: none; + -webkit-appearance: none; +} +.searchbox-input::placeholder { color: var(--color-ink-4); } +.searchbox-input::-webkit-search-decoration, +.searchbox-input::-webkit-search-cancel-button, +.searchbox-input::-webkit-search-results-button { + appearance: none; + -webkit-appearance: none; + display: none; +} + +.searchbox-clear { + flex: none; + display: grid; + place-items: center; + width: 18px; + height: 18px; + padding: 0; + border: 0; + border-radius: 999px; + background: var(--color-surface-sunken); + color: var(--color-ink-3); + cursor: pointer; + transition: background 140ms ease, color 140ms ease; +} +.searchbox-clear:hover { background: var(--color-line); color: var(--color-ink-1); } +.searchbox-clear:focus-visible { outline: 2px solid var(--color-brand); outline-offset: 1px; } + +/* On a phone the header row wraps, and a 260px box beside a scrolling tab + track leaves neither enough room. It takes the full line instead. */ +@media (max-width: 600px) { + .searchbox { width: 100% !important; } +} diff --git a/src/components/shell/AppShell.tsx b/src/components/shell/AppShell.tsx new file mode 100644 index 0000000..fd153b0 --- /dev/null +++ b/src/components/shell/AppShell.tsx @@ -0,0 +1,824 @@ +import { useEffect, useRef, useState, type ReactNode } from 'react'; +import { Link, NavLink, Outlet, useLocation } from 'react-router-dom'; +import { ErrorBoundary } from '@/components/ErrorBoundary'; +import { ChevronDown, ChevronLeft, LogOut, Menu, X } from 'lucide-react'; +import { useIsMobile } from '@/hooks/useIsMobile'; +import { useAuth } from '@/auth/AuthContext'; +import { ROLE_LABEL } from '@/auth/roles'; +import { AssistantPanel } from './AssistantPanel'; +import { DateScopePicker } from './DateScope'; + +export interface NavEntry { + to: string; + label: string; +} + +/** One entry in the account menu's MANAGE group. */ +export interface MenuEntry { + to: string; + label: string; + icon: ReactNode; + /** One line under the label, for an entry whose scope is not obvious. */ + note?: string; +} + +export interface AppShellProps { + /** Destinations for the header tabs and the mobile sheet. */ + nav: readonly NavEntry[]; + /** Where the logo links to — the workspace's own landing page. */ + home: string; + /** Accessible name for the tab list, e.g. "Store Admin". */ + navLabel: string; + /** + * An optional control between the logo and the tabs — the Store Admin's + * branch selector lives here. It sits inside the header rather than on each + * page because it scopes every page, and a control that moves between pages + * reads as a different control each time. + */ + scopeControl?: ReactNode; + /** + * Setup destinations, listed inside the account menu rather than in the nav. + * + * This follows the old console's judgement, and its reasoning holds: setup is + * configuration, not a place anyone works from day to day, and a nav slot + * spent on it is a slot taken from a section that IS worked from. The account + * menu is where "my workspace's setup" belongs, beside the account and + * sign-out. + * + * Listed as individual destinations rather than one "Settings" entry for the + * old console's other reason: a single entry lands everyone on the first + * section and makes them click again, and the sections are what people come + * here for. + */ + manageItems?: readonly MenuEntry[]; + /** + * Workspace-specific controls in the header, left of the notification bell. + * + * For things a shop reaches from anywhere and that open in place rather than + * navigating — the store's QR code is the first. A page for it would have + * been a fifth destination for something that is looked at, printed once, and + * closed. + */ + headerActions?: ReactNode; + /** + * A full-width strip between the header and the page. + * + * The setup walkthrough lives here. It has to sit in the shell rather than + * on a page because it crosses several: one step is on Profile, the next on + * Users, the next on Inventory — anything page-local would vanish the moment + * somebody followed it. + */ + banner?: ReactNode; +} + +/** + * The application chrome, shared by every workspace. + * + * Built to KROW's `AdminLayout` spec. Deliberately NOT a coloured slab: the + * header is a 56px white-at-85% bar with a backdrop blur and a hairline bottom + * border, carrying text tabs whose active state is accent-coloured text plus a + * 2px accent bar sitting on that border. The repo's own reasoning for text tabs + * over pills: several destinations means several competing shapes if each one + * is a pill, and a console header should recede rather than compete with the + * page. + * + * One component rather than one per workspace. The workspaces differ in exactly + * three things — their destinations, their home, and whether they have a scope + * control — so those are props. Copying four hundred lines of chrome per role + * is how two headers drift apart and stop looking like one product. + */ +export function AppShell({ + nav, + home, + navLabel, + scopeControl, + manageItems, + headerActions, + banner, +}: AppShellProps) { + const { user, signOut } = useAuth(); + const { pathname } = useLocation(); + + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isAssistantOpen, setIsAssistantOpen] = useState(true); + const [isNavOpen, setIsNavOpen] = useState(false); + const isMobile = useIsMobile(); + const menuRef = useRef(null); + + /* + A new page starts at the top. + + A single-page app does not reload, so the document keeps whatever scroll + offset the last page was left at: read Sales down to row 40, click Inventory, + and Inventory opens 2,000px down — usually past everything it has, so it + looks empty. Nothing in the app was resetting this. + + INSTANT, deliberately, and it is the one place in the console that opts out + of the smooth scrolling `index.css` turns on. `behavior: 'smooth'` here would + animate those 2,000px on every navigation, which means a second of the new + page flying past before it settles — the page arriving late, rather than the + page arriving. Smooth is for a move you asked for within a page; a route + change is not one. + + Keyed on `pathname` alone, not on the whole location: the branch scope and + the date range travel in the query string, and re-scoping a table you are + halfway down should leave you where you were. + */ + useEffect(() => { + window.scrollTo({ top: 0, left: 0, behavior: 'instant' }); + }, [pathname]); + + // Escape closes the account menu. A menu that can only be dismissed by + // finding the trigger again is a trap for anyone on a keyboard, and this one + // sits over the page rather than beside it. + useEffect(() => { + if (!isMenuOpen) return; + function onKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape') setIsMenuOpen(false); + } + function onMouseDown(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsMenuOpen(false); + } + } + window.addEventListener('keydown', onKeyDown); + document.addEventListener('mousedown', onMouseDown); + return () => { + window.removeEventListener('keydown', onKeyDown); + document.removeEventListener('mousedown', onMouseDown); + }; + }, [isMenuOpen]); + + return ( +
+ {/* Two elements, on purpose. The bar is full-bleed — the glass, the blur + and the hairline run the whole width of the screen, because a header + that stops short of the edge reads as a floating card, not as chrome. + The ROW inside it is capped by `.app-gutter`, so the logo and nav sit + on exactly the same left edge as the page title below them at every + width, including a 2560px monitor where the body is centred. */} + {/* + Opaque, with no backdrop blur. + + The blur cost more than it bought the moment a popover moved into this + bar. `backdrop-filter` makes an element a CONTAINING BLOCK for every + `position: fixed` descendant, and the design system's popovers are fixed + and CSS-anchor-positioned — so the date picker's calendar rendered + inside the header at 0×0 and could not be opened at all. The same + control worked perfectly two pixels lower, on the page. + + A solid background is the fix rather than a hack around it: the bar sits + on a near-white page, so at 85% opacity plus blur it was already almost + opaque, and nothing here reads differently for losing it. + */} +
+
+ + {/* 24px tall, width auto — the reference pins the logo's height and + lets the wordmark set its own width. */} + Nearle + + + + {/* Below md the scope control moves into the navigation sheet. On a + 390px phone the logo, the selector and the right-hand cluster add + up to 503px and push the header 113px past the viewport, so the + page scrolls sideways. The sheet gives the branch names room to be + read in full, and every page states its own scope in the header + line beneath, so the phone never leaves the operator guessing. */} + {scopeControl ? ( +
+ {scopeControl} +
+ ) : null} + + {/* Below lg the tabs are gone, so a flexible spacer keeps the right + cluster against the edge instead of bunched beside the logo. */} +
+ + {/* Text tabs. Active = accent text + a 2px accent bar on the header rule. */} + + +
+ {/* No global search. + + There was a search box here with `⌘K` on it, and its `onSubmit` + was `preventDefault()` and nothing else — it advertised a + console-wide search that did not exist and had no endpoint + behind it. The per-page search boxes on Sales, Users, Stores and + the catalogue are real and stay. A control that looks like it + works costs more trust than a missing one. */} + + {/* The date filter, beside the profile and common to every page. + + Rendered here rather than by each page so the two questions the + console asks — which shop, and when — are both answered in the + chrome, and so the answer survives navigation. */} + + + {headerActions} + + {/* No notification bell. It was labelled "2 unread" with the dot + painted unconditionally, for every user on every page, forever — + and it had no click handler and no notifications endpoint behind + it anywhere in the API. */} + + {/* Assistant button moved to floating pill */} + + {/* Avatar trigger + chevron, with the account menu below it. */} +
+ + + {isMenuOpen ? ( +
+
+
+ {user?.name} +
+
{user?.email}
+
+ + {user ? ROLE_LABEL[user.role] : ''} + +
+
+ {manageItems && manageItems.length > 0 ? ( + <> + +

+ Manage +

+ {manageItems.map((entry) => ( + setIsMenuOpen(false)} + /> + ))} + + ) : null} + + + + + {/* When this code was built. If it does not match what you + were told was delivered, you are looking at an old copy — + which is not something any screen otherwise reveals. */} +

+ Build {__BUILD_STAMP__} +

+
+ ) : null} +
+ + {/* Wrapped rather than styled inline: an inline `display` would win + over the media query that hides this above the tabs breakpoint. */} + + setIsNavOpen(true)}> + + + +
+
+
+ + {/* Body: a column on a phone so the assistant stacks under the page, a + row from md where it becomes a side column. */} + {banner} + +
+
+ {/* Scoped to the page, not the shell: a page that throws should leave + the nav, the account menu and the workspace switcher usable, so + you can walk to a screen that works instead of reloading blind. + Keyed by pathname so navigating away clears a caught error — + without that, one broken page latches the whole outlet. */} + + + +
+ + {isAssistantOpen || isMobile ? ( + setIsAssistantOpen(false)} isStacked={isMobile} /> + ) : null} +
+ + {/* Floating Assistant Button */} + {!isMobile && !isAssistantOpen ? ( + + ) : null} + + {isNavOpen ? ( + setIsNavOpen(false)} + pathname={pathname} + /> + ) : null} +
+ ); +} + +/** + * The navigation sheet, below lg. + * + * 288px from the right, opaque rather than glass — a translucent sheet over a + * page of tables is unreadable — with the active item carried by a tinted fill + * instead of the 2px underline, which has nothing to sit on here. + */ +function MobileNav({ + nav, + scopeControl, + onClose, + pathname, +}: { + nav: readonly NavEntry[]; + scopeControl?: ReactNode; + onClose: () => void; + pathname: string; +}) { + return ( +
+
+ ); +} + +/** + * One destination in the account menu. + * + * A `NavLink` rather than a button with `navigate()`: middle-click, ⌘-click and + * "open in new tab" all work on a real anchor and none of them work on a + * button, and a setup screen is exactly the kind of thing someone parks in a + * second tab. + */ +function MenuLink({ entry, onNavigate }: { entry: MenuEntry; onNavigate: () => void }) { + const [isHovered, setIsHovered] = useState(false); + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + style={{ + display: 'flex', + alignItems: 'center', + gap: 10, + padding: '8px 12px', + borderRadius: 12, + textDecoration: 'none', + background: isHovered ? 'var(--color-surface-sunken)' : 'transparent', + color: 'var(--color-ink-1)', + transition: 'background .15s', + }} + > + + {entry.icon} + + + {entry.label} + {entry.note ? ( + + {entry.note} + + ) : null} + + + ); +} + +const Rule = () => ( +
+); + +/** + * Up to two letters for the avatar. Never a dash. + * + * First name and last name where the account has them. Where it does not — + * plenty of rows on this backend carry neither — the email is used instead, + * split on the separators people actually put in addresses, so + * `ragul.kumar@shop.in` gives RK and `care@nearle.in` gives C. + * + * The old version returned an em dash for anything it could not parse, and that + * is what every Store Admin and Store user saw: their accounts have no first or + * last name, so the avatar was a dash on every page. A dash says nothing and + * looks like a bug, which is worse than one letter. + */ +function initials(name: string): string { + const trimmed = name.trim(); + if (trimmed === '') return '?'; + + // An email is not a name. Take the part before the @ and read the words out + // of it — a full address would otherwise give the domain's letter as the + // second initial. + const source = trimmed.includes('@') ? (trimmed.split('@')[0] ?? trimmed) : trimmed; + + const words = source + .split(/[\s._\-+]+/) + .map((word) => word.replace(/[^\p{L}\p{N}]/gu, '')) + .filter(Boolean); + + if (words.length === 0) return '?'; + + const first = words[0]?.[0] ?? ''; + const last = words.length > 1 ? (words[words.length - 1]?.[0] ?? '') : ''; + return (first + last).toUpperCase(); +} + +/** + * A 32px icon-only control. + * + * `label` is required, not optional — an icon-only control with no accessible + * name is a bug, so the API makes it impossible to omit. + */ +export function IconButton({ + label, + children, + onClick, + hasUnread, +}: { + label: string; + children: ReactNode; + onClick?: () => void; + hasUnread?: boolean; +}) { + const [isHovered, setIsHovered] = useState(false); + return ( + + ); +} diff --git a/src/components/shell/AssistantPanel.tsx b/src/components/shell/AssistantPanel.tsx new file mode 100644 index 0000000..b7e8c93 --- /dev/null +++ b/src/components/shell/AssistantPanel.tsx @@ -0,0 +1,1009 @@ +import { Fragment, useEffect, useRef, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { errorMessage } from '@/api/client'; +import { + approveAssistant, + askAssistant, + assistantAvailable, + type AssistantAnswer, +} from '@/api/assistant'; +import { useAssistantScope } from './assistantScope'; +import { matchAssistantRoute } from './assistantContext'; +import { ArrowUp, History, Maximize2, Minimize2, PanelRightClose } from 'lucide-react'; +import { + DEFAULT_WIDTH, + expandedWidth, + MAX_WIDTH, + MIN_WIDTH, + useAssistantWidth, +} from './assistantWidth'; + +/** One question and what came back. `answer` and `error` are exclusive. */ +interface Exchange { + question: string; + answer?: AssistantAnswer; + error?: string; + /** + * What became of a change Buddy proposed. + * + * `undefined` means the card is still on screen waiting. Once decided the + * card is replaced by its outcome and cannot be pressed again — a card that + * stayed live after approval is a second write waiting to happen. + */ + decision?: { state: 'approving' | 'done' | 'dismissed' | 'failed'; message?: string }; +} + +/** Replaces the last entry, which is always the one in flight. */ +function replaceLast(thread: Exchange[], update: (entry: Exchange) => Exchange): Exchange[] { + if (thread.length === 0) return thread; + return thread.map((entry, index) => (index === thread.length - 1 ? update(entry) : entry)); +} + +/** + * What the composer says when it cannot be used, and why. + * + * Three different states with three different sentences. "Not connected yet" + * for everything would be true and useless: a person whose deployment has no + * model can do nothing, but a person on the Inventory page can move to Sales + * and get an answer today. + */ +export function composerHint( + available: boolean | null, + hasAgent: boolean, + isAsking: boolean, +): string { + if (isAsking) return 'Thinking…'; + if (available === null) return 'Checking…'; + if (available === false) return 'Not connected yet'; + if (!hasAgent) return 'No assistant for this page yet'; + return 'Ask about this page'; +} + +/** + * The line under the composer, or nothing. + * + * This used to be one hardcoded sentence — "Not connected yet — there is no + * assistant service behind this panel." — written before the panel had an API + * behind it and never removed once it did. It rendered on every page in every + * state, so the panel told everybody Buddy was off, permanently, including on a + * deployment where Buddy was answering questions. It also flatly contradicted + * the placeholder three lines above it, which was the only part telling the + * truth. + * + * The lesson worth keeping: a status message with no condition attached is not + * a status message. Each branch here reads the same two facts the composer + * itself is disabled by, so the two cannot drift apart again. + */ +export function composerNote(available: boolean | null, hasAgent: boolean): string { + // Still asking. A sentence that appears for 200ms and is replaced reads as a + // flicker, so the space stays empty until there is something true to put in it. + if (available === null) return ''; + if (available === false) return 'Not connected yet — there is no assistant service behind this panel.'; + if (!hasAgent) return 'No assistant for this page yet. Try Console or Sales.'; + // Buddy is working. The caveat that matters then is not about connection: it + // is that an answer is a model reading live rows, and the rows are the thing + // to act on. + return 'Buddy reads your live data. Check anything you act on.'; +} + +/** + * A change waiting on the person. + * + * Framed and set apart from the reply on purpose. The sentence above it was + * written by a model; this was resolved by the server against the database, and + * the two must not read as one thing. What is shown here — the ids, the names, + * the quantity — is what the button actually agrees to. + * + * Once decided the buttons are gone, replaced by what happened. A card that + * stayed pressable after approval is a second write waiting for a double-click. + */ +function ApprovalCard({ + proposal, + decision, + onDecide, +}: { + proposal: NonNullable; + decision: Exchange['decision']; + onDecide: (approve: boolean) => void; +}) { + const busy = decision?.state === 'approving'; + + const outcomeColour = + decision?.state === 'failed' + ? 'var(--color-error, #d64545)' + : decision?.state === 'done' + ? 'var(--color-success, #1f9d55)' + : 'var(--color-ink-3)'; + + return ( +
+

+ {proposal.summary} +

+ + {proposal.details?.length ? ( +
+ {proposal.details.map((detail) => ( + +
{detail.label}
+
+ {detail.value} +
+
+ ))} +
+ ) : null} + + {proposal.warning ? ( +

+ {proposal.warning} +

+ ) : null} + + {decision === undefined ? ( +
+ + {/* "Not now", not "Reject". Nothing is refused and nothing is + recorded — the card is simply left alone, and it expires unused. */} + +
+ ) : ( +

+ {busy + ? 'Making the change…' + : decision.state === 'dismissed' + ? 'Left alone. Nothing was changed.' + : (decision.message ?? (decision.state === 'done' ? 'Done.' : 'That did not go through.'))} +

+ )} +
+ ); +} + +/** + * One question and its answer. + * + * The answer shows what it ran, not just what it concluded. Buddy states things + * with the confidence of a sentence, and the only honest way to present that is + * beside the tools it used and a link to the page holding the same rows — so a + * person can disagree with it. + */ +function Exchange({ + entry, + isLast, + isAsking, + onDecide, +}: { + entry: Exchange; + isLast: boolean; + isAsking: boolean; + onDecide: (approve: boolean) => void; +}) { + const waiting = isLast && isAsking && !entry.answer && !entry.error; + + return ( +
+

+ {entry.question} +

+ + {waiting ? ( +

Looking…

+ ) : null} + + {entry.error ? ( +

+ {entry.error} +

+ ) : null} + + {entry.answer ? ( +
+ ) : null} +
+ ); +} + +/** + * Typed against `CONTEXT`'s own shape so the two cannot drift. + * + * Without this the fallback has no `agent` key at all, and `routeContext` — + * which is one or the other — loses the field entirely. A page that fell + * through to the fallback would then be a compile error rather than simply a + * page with no assistant, which is what it is. + */ + +/** + * Nearle Buddy, as a layout column. + * + * Not an overlay and not a floating bubble: the panel is part of the page on + * every supported route, so the page reflows beside it rather than being + * covered. It opens by stating the most useful thing about the page it is next + * to, says what it read, and offers prompts scoped to that page. + * + * 380px by default, sticky under the 56px header, and its own height is the + * viewport minus 104px so it never pushes the page taller than the screen. + */ +export function AssistantPanel({ + onClose, + isStacked = false, +}: { + onClose: () => void; + /** Below md the panel sits under the page rather than beside it. */ + isStacked?: boolean; +}) { + const { pathname } = useLocation(); + const [draft, setDraft] = useState(''); + const [isFocused, setIsFocused] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); + const textareaRef = useRef(null); + const threadRef = useRef(null); + const { width, setWidth, reset } = useAssistantWidth(); + const [isDragging, setDragging] = useState(false); + + const { key, context: routeContext } = matchAssistantRoute(pathname); + + /** + * The heading follows the branch in view. + * + * A panel headed "Across your branches" sitting above a board showing one + * shop is a promise about scope that the page has already broken. Only the + * title moves; everything else about Buddy is untouched. + */ + const scopeLabel = useAssistantScope(); + const context = scopeLabel ? { ...routeContext, title: scopeLabel } : routeContext; + + /* + * Whether Buddy can answer here at all, and why not when it cannot. + * + * Two separate reasons, kept apart because they are two different facts and + * the person can act on one of them. `available === false` means this + * deployment has no model configured — nothing to be done from the browser. + * `agent === undefined` means this page has no assistant yet, which is about + * the page and not the deployment. + * + * `null` is "not asked yet": the composer stays disabled during the check, so + * it is never briefly live against a server that turns out to have no model. + */ + const [available, setAvailable] = useState(null); + const [thread, setThread] = useState([]); + const [isAsking, setAsking] = useState(false); + const agent = routeContext.agent; + const canAsk = available === true && Boolean(agent) && !isAsking; + + useEffect(() => { + let live = true; + void assistantAvailable().then((ok) => { + if (live) setAvailable(ok); + }); + // Cancelled on unmount so a slow answer cannot set state on a closed panel. + return () => { + live = false; + }; + }, []); + + /* + * The thread is per-page and deliberately not persisted. + * + * An answer about Sales sitting above the Inventory page is worse than no + * answer: it reads as being about what is on screen. Clearing on navigation + * costs a person their history, which is the smaller loss. + */ + useEffect(() => { + setThread([]); + }, [key]); + + /* + * Follow the conversation down, unless the person has scrolled away from it. + * + * A chat that does not follow leaves the newest answer below the fold, which + * reads as nothing having happened. One that follows unconditionally yanks + * somebody out of an earlier answer they were still reading the moment a + * reply lands — and a reply can land a while after the question, because a + * tool call and a model round trip are seconds, not milliseconds. + * + * So: only when they were already at the bottom. The 40px allowance covers + * fractional scroll heights, which browsers disagree about by a pixel or two + * at non-integer zoom levels — without it this silently stops following for + * anybody not at 100%. + */ + useEffect(() => { + const el = threadRef.current; + if (!el) return; + + const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; + if (distanceFromBottom > 40) return; + + el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' }); + }, [thread]); + + /* + * Approving is its own call, with no question in it. + * + * Indexed rather than acting on the last exchange: a person can scroll up and + * approve an earlier card, and nothing about a card ties it to being the most + * recent thing said. + */ + const decide = async (index: number, approve: boolean) => { + const entry = thread[index]; + const card = entry?.answer?.awaiting?.card; + if (!card || !agent || entry.decision) return; + + if (!approve) { + // Dismissing is purely local: the server was never told, because there is + // nothing to undo. The card simply expires unused. + setThread((current) => + current.map((e, i) => (i === index ? { ...e, decision: { state: 'dismissed' as const } } : e)), + ); + return; + } + + setThread((current) => + current.map((e, i) => (i === index ? { ...e, decision: { state: 'approving' as const } } : e)), + ); + + try { + const done = await approveAssistant(agent, card); + setThread((current) => + current.map((e, i) => + i === index ? { ...e, decision: { state: 'done' as const, message: done.reply } } : e, + ), + ); + } catch (error) { + // "Somebody already approved that" arrives here, and it is an answer + // rather than a fault — shown on the card, which stays decided so the + // button cannot be pressed again into the same refusal. + setThread((current) => + current.map((e, i) => + i === index ? { ...e, decision: { state: 'failed' as const, message: errorMessage(error) } } : e, + ), + ); + } + }; + + const send = async (text: string) => { + const question = text.trim(); + if (!question || !agent || !canAsk) return; + + setDraft(''); + setAsking(true); + setThread((current) => [...current, { question }]); + + try { + const answer = await askAssistant(agent, question); + setThread((current) => replaceLast(current, (entry) => ({ ...entry, answer }))); + } catch (error) { + // Shown in the thread rather than as a toast: the question is still on + // screen, and the failure belongs next to it. + setThread((current) => + replaceLast(current, (entry) => ({ ...entry, error: errorMessage(error) })), + ); + } finally { + setAsking(false); + } + }; + + /** + * Three widths, in priority order: stacked (the phone layout owns it), + * expanded (the one-click half-screen), then whatever the operator dragged + * it to. The inline width beats the media queries in `index.css`, which is + * the point — those are the default, not the rule. + * + * Height is deliberately NOT set here. It was, briefly — measured off the + * page so a short page got a short panel — but that put a strip of page + * background under the panel on every screen that was not full. The column + * is full height, always, and `.assistant` owns that. + */ + const sizeStyle: React.CSSProperties = isStacked + ? {} + : isExpanded + ? { width: expandedWidth(window.innerWidth), maxWidth: 'none' } + : { width, maxWidth: 'none' }; + + return ( + /* No `position` here. `.assistant` is `position: sticky` from md up, and an + inline `relative` silently beat it — which is what made the panel scroll + away with the page. Sticky is itself a containing block, so the resize + handle's `absolute` still anchors to it. */ +