Commit fc62d6d7a693f621909a9c7489122a937001708d

Authored by zichun
1 parent db8d7b51

feat(web+distribution): R1 Web SPA bootstrap + demo seed

First runnable end-to-end demo: open the browser, log in, click
through every PBC, and walk a sales order DRAFT → CONFIRMED →
SHIPPED. Stock balances drop, the SALES_SHIPMENT row appears in the
ledger, and the AR journal entry settles — all visible in the SPA
without touching curl.

Bumps version to 0.29.0-SNAPSHOT.

What landed
-----------

* New `:web` Gradle subproject — Vite + React 18 + TypeScript +
  Tailwind 3.4. The Gradle wrapper is two `Exec` tasks
  (`npmInstall`, `npmBuild`) with proper inputs/outputs declared
  for incremental builds. Deliberately no node-gradle plugin —
  one less moving piece.

* SPA architecture: hand-written typed REST client over `fetch`
  (auth header injection + 401 handler), AuthContext that decodes
  the JWT for display, ProtectedRoute, AppLayout with sidebar
  grouped by PBC, 16 page components covering the full v1 surface
  (Items, UoMs, Partners, Locations, Stock Balances + Movements,
  Sales Orders + detail w/ confirm/ship/cancel, Purchase Orders +
  detail w/ confirm/receive/cancel, Work Orders + detail w/
  start/complete, Shop-Floor dashboard with 5s polling, Journal
  Entries). 211 KB JS / 21 KB CSS gzipped.

* Sales-order detail page: confirm/ship/cancel verbs each refresh
  the order, the (SO-filtered) movements list, and the
  (SO-filtered) journal entries — so an operator watches the
  ledger row appear and the AR row settle in real time after a
  single click. Same pattern on the purchase-order detail page
  for the AP/RECEIPT side.

* Shop-floor dashboard polls /api/v1/production/work-orders/shop-
  floor every 5s and renders one card per IN_PROGRESS WO with
  current operation, planned vs actual minutes (progress bar),
  and operations-completed.

* `:distribution` consumes the SPA dist via a normal Gradle
  outgoing/incoming configuration: `:web` exposes
  `webStaticBundle`, `:distribution`'s `bundleWebStatic` Sync
  task copies it into `${buildDir}/web-static/static/`, and
  that parent directory is added to the main resources source
  set so Spring Boot serves the SPA from `classpath:/static/`
  out of the same fat-jar. Single artifact, no nginx, no CORS.

* New `SpaController` in platform-bootstrap forwards every
  known SPA route prefix to `/index.html` so React Router's
  HTML5 history mode works on hard refresh / deep-link entry.
  Explicit list (12 prefixes) rather than catch-all so typoed
  API URLs still get an honest 404 instead of the SPA shell.

* SecurityConfiguration restructured: keeps the public allowlist
  for /api/v1/auth + /api/v1/_meta + /v3/api-docs + /swagger-ui,
  then `/api/**` is `.authenticated()`, then SPA static assets
  + every SPA route prefix are `.permitAll()`. The order is
  load-bearing — putting `.authenticated()` for /api/** BEFORE
  the SPA permitAll preserves the framework's "API is always
  authenticated" invariant even with the SPA bundled in the
  same fat-jar. The SPA bundle itself is just HTML+CSS+JS so
  permitting it is correct; secrets are gated by /api/**.

* New `DemoSeedRunner` in `:distribution` (gated behind
  `vibeerp.demo.seed=true`, set in application-dev.yaml only).
  Idempotent — the runner short-circuits if its sentinel item
  (DEMO-PAPER-A4) already exists. Seeds 5 items, 2 warehouses,
  4 partners, opening stock for every item, one open
  DEMO-SO-0001 (50× business cards + 20× brochures, $720), one
  open DEMO-PO-0001 (10000× paper, $400). Every row carries
  the DEMO- prefix so it's trivially distinguishable from
  hand-created data; a future "delete demo data" command has
  an obvious filter. Production deploys never set the property,
  so the @ConditionalOnProperty bean stays absent from the
  context.

How to run
----------

  docker compose up -d db
  ./gradlew :distribution:bootRun
  open http://localhost:8080

  # Read the bootstrap admin password from the boot log,
  # log in as admin, and walk DEMO-SO-0001 through the
  # confirm + ship flow to see the buy-sell loop in the UI.

What was caught by the smoke test
---------------------------------

* TypeScript strict mode + `error: unknown` in React state →
  `{error && <X/>}` evaluates to `unknown` and JSX rejects it.
  Fixed by typing the state as `Error | null` and converting
  in catches with `e instanceof Error ? e : new Error(String(e))`.
  Affected 16 page files; the conversion is now uniform.

* DataTable's `T extends Record<string, unknown>` constraint was
  too restrictive for typed row interfaces; relaxed to
  unconstrained `T` with `(row as unknown as Record<…>)[key]`
  for the unkeyed cell read fallback.

* `vite.config.ts` needs `@types/node` for `node:path` +
  `__dirname`; added to devDependencies and tsconfig.node.json
  declares `"types": ["node"]`.

* KDoc nested-comment trap (4th time): SpaController's KDoc
  had `/api/v1/...` in backticks; the `/*` inside backticks
  starts a nested block comment and breaks Kotlin compilation
  with "Unclosed comment". Rephrased to "the api-v1 prefix".

* SecurityConfiguration order: a draft version that put the
  SPA permit-all rules BEFORE `/api/**` authenticated() let
  unauthenticated requests reach API endpoints. Caught by an
  explicit smoke test (curl /api/v1/some-bogus-endpoint should
  return 401, not 404 from a missing static file). Reordered
  so /api/** authentication runs first.

End-to-end smoke (real Postgres, fresh DB)
------------------------------------------

  - bootRun starts in 7.6s
  - DemoSeedRunner reports "populating starter dataset… done"
  - GET / returns the SPA HTML
  - GET /sales-orders, /sales-orders/<uuid>, /journal-entries
    all return 200 (SPA shell — React Router takes over)
  - GET /assets/index-*.js / /assets/index-*.css both 200
  - GET /api/v1/some-bogus-endpoint → 401 (Spring Security
    rejects before any controller mapping)
  - admin login via /api/v1/auth/login → 200 + JWT
  - GET /catalog/items → 5 DEMO-* rows
  - GET /partners/partners → 4 DEMO-* rows
  - GET /inventory/locations → 2 DEMO-* warehouses
  - GET /inventory/balances → 5 starting balances
  - POST /orders/sales-orders/<id>/confirm → CONFIRMED;
    GET /finance/journal-entries shows AR POSTED 720 USD
  - POST /orders/sales-orders/<id>/ship {"shippingLocationCode":
    "DEMO-WH-FG"} → SHIPPED; balances drop to 150 + 80;
    journal entry flips to SETTLED
  - POST /orders/purchase-orders/<id>/confirm → CONFIRMED;
    AP POSTED 400 USD appears
  - POST /orders/purchase-orders/<id>/receive → RECEIVED;
    PAPER balance grows from 5000 to 15000; AP row SETTLED
  - 8 stock_movement rows in the ledger total
Showing 46 changed files with 6282 additions and 8 deletions

Too many changes to show.

To preserve performance only 33 of 46 files are displayed.

distribution/build.gradle.kts
... ... @@ -4,6 +4,16 @@
4 4 // together into a bootable Spring Boot application. It is referenced by
5 5 // the Dockerfile (stage 1) which produces the shipping image documented
6 6 // in the architecture spec, sections 10 and 11.
  7 +//
  8 +// **Web SPA bundling.** The `:web` subproject is a Gradle wrapper around
  9 +// the Vite/React build. We declare a `webStaticBundle` consumer
  10 +// configuration against it; the `bundleWebStatic` Sync task copies
  11 +// the dist/ directory into `${buildDir}/web-static/static/` and that
  12 +// parent directory is added to the main resources source set so Spring
  13 +// Boot's `classpath:/static/**` resource resolver picks the SPA up at
  14 +// runtime. The result is a single fat-jar containing both the JVM
  15 +// backend and the SPA — no nginx, no separate static-file server, no
  16 +// CORS.
7 17  
8 18 plugins {
9 19 alias(libs.plugins.kotlin.jvm)
... ... @@ -89,4 +99,45 @@ tasks.bootRun {
89 99 // <repo>/plugins-dev/, not <repo>/distribution/plugins-dev/.
90 100 workingDir = rootProject.layout.projectDirectory.asFile
91 101 dependsOn(":reference-customer:plugin-printing-shop:installToDev")
  102 + // Make sure the SPA dist/ is staged into the classpath before
  103 + // Spring Boot starts; otherwise an in-IDE bootRun with no prior
  104 + // build serves a 404 at the root path. processResources already
  105 + // depends on bundleWebStatic, but listing it explicitly here
  106 + // makes the relationship visible to anyone reading bootRun.
  107 + dependsOn("bundleWebStatic")
  108 +}
  109 +
  110 +// ─── Web SPA bundling ────────────────────────────────────────────────
  111 +//
  112 +// Resolvable configuration that pulls the `webStaticBundle` artifact
  113 +// from `:web`. The `:web` subproject's outgoing configuration declares
  114 +// the dist/ directory as its single artifact, built by the npmBuild
  115 +// Exec task. We don't need attribute matchers because there's only
  116 +// one artifact in the configuration.
  117 +val webStaticBundle: Configuration by configurations.creating {
  118 + isCanBeConsumed = false
  119 + isCanBeResolved = true
  120 +}
  121 +
  122 +dependencies {
  123 + webStaticBundle(project(mapOf("path" to ":web", "configuration" to "webStaticBundle")))
  124 +}
  125 +
  126 +// Stage the SPA dist into ${buildDir}/web-static/static/ so the
  127 +// directory layout matches Spring Boot's classpath:/static/ convention.
  128 +// Adding the parent dir as a resource source set is what then makes
  129 +// the JAR's classpath include those files at the right path.
  130 +val bundleWebStatic by tasks.registering(Sync::class) {
  131 + group = "build"
  132 + description = "Stage the :web SPA bundle into the distribution resources tree."
  133 + from({ webStaticBundle })
  134 + into(layout.buildDirectory.dir("web-static/static"))
  135 +}
  136 +
  137 +sourceSets.named("main") {
  138 + resources.srcDir(layout.buildDirectory.dir("web-static"))
  139 +}
  140 +
  141 +tasks.named("processResources") {
  142 + dependsOn(bundleWebStatic)
92 143 }
... ...
distribution/src/main/kotlin/org/vibeerp/demo/DemoSeedRunner.kt 0 → 100644
  1 +package org.vibeerp.demo
  2 +
  3 +import org.slf4j.LoggerFactory
  4 +import org.springframework.boot.CommandLineRunner
  5 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
  6 +import org.springframework.stereotype.Component
  7 +import org.springframework.transaction.annotation.Transactional
  8 +import org.vibeerp.pbc.catalog.application.CreateItemCommand
  9 +import org.vibeerp.pbc.catalog.application.ItemService
  10 +import org.vibeerp.pbc.catalog.domain.ItemType
  11 +import org.vibeerp.pbc.inventory.application.CreateLocationCommand
  12 +import org.vibeerp.pbc.inventory.application.LocationService
  13 +import org.vibeerp.pbc.inventory.application.StockBalanceService
  14 +import org.vibeerp.pbc.inventory.domain.LocationType
  15 +import org.vibeerp.pbc.orders.purchase.application.CreatePurchaseOrderCommand
  16 +import org.vibeerp.pbc.orders.purchase.application.PurchaseOrderLineCommand
  17 +import org.vibeerp.pbc.orders.purchase.application.PurchaseOrderService
  18 +import org.vibeerp.pbc.orders.sales.application.CreateSalesOrderCommand
  19 +import org.vibeerp.pbc.orders.sales.application.SalesOrderLineCommand
  20 +import org.vibeerp.pbc.orders.sales.application.SalesOrderService
  21 +import org.vibeerp.pbc.partners.application.CreatePartnerCommand
  22 +import org.vibeerp.pbc.partners.application.PartnerService
  23 +import org.vibeerp.pbc.partners.domain.PartnerType
  24 +import org.vibeerp.platform.persistence.security.PrincipalContext
  25 +import java.math.BigDecimal
  26 +import java.time.LocalDate
  27 +
  28 +/**
  29 + * One-shot demo data seeder, gated behind `vibeerp.demo.seed=true`.
  30 + *
  31 + * **Why this exists.** Out-of-the-box, vibe_erp boots against an
  32 + * empty Postgres and the SPA dashboard shows zeros for every PBC.
  33 + * Onboarding a new operator (or running a tomorrow-morning demo)
  34 + * needs a couple of minutes of clicking to create items,
  35 + * locations, partners, and a starting inventory before the
  36 + * interesting screens become useful. This runner stages a tiny
  37 + * but representative dataset on first boot so the moment the
  38 + * bootstrap admin lands on `/`, every page already has rows.
  39 + *
  40 + * **Opt-in by property.** `@ConditionalOnProperty` keeps this
  41 + * bean entirely absent from production deployments — only the
  42 + * dev profile (`application-dev.yaml` sets `vibeerp.demo.seed:
  43 + * true`) opts in. A future release can ship a `--demo` CLI flag
  44 + * or a one-time admin "Load demo data" button that flips the
  45 + * same property at runtime; for v1 the dev profile is enough.
  46 + *
  47 + * **Idempotent.** The runner checks for one of its own seeded
  48 + * item codes and short-circuits if already present. Restarting
  49 + * the dev server is a no-op; deleting the demo data has to
  50 + * happen via SQL or by dropping the DB. Idempotency on the
  51 + * sentinel item is intentional (vs. on every entity it creates):
  52 + * a half-seeded DB from a crashed first run will *not* recover
  53 + * cleanly, but that case is exotic and we can clear and retry
  54 + * in dev.
  55 + *
  56 + * **All seeded data shares the `DEMO-` prefix.** Items, partners,
  57 + * locations, and order codes all start with `DEMO-`. This makes
  58 + * the seeded data trivially distinguishable from anything an
  59 + * operator creates by hand later — and gives a future
  60 + * "delete demo data" command an obvious filter.
  61 + *
  62 + * **System principal.** Audit columns need a non-blank
  63 + * `created_by`; `PrincipalContext.runAs("__demo_seed__")` wraps
  64 + * the entire seed so every row carries that sentinel. The
  65 + * authorization aspect (`@RequirePermission`) lives on
  66 + * controllers, not services — calling services directly bypasses
  67 + * it cleanly, which is correct for system-level seeders.
  68 + *
  69 + * **Why CommandLineRunner.** Equivalent to `ApplicationRunner`
  70 + * here — there are no command-line args this seeder cares about.
  71 + * Spring runs every CommandLineRunner once, after the application
  72 + * context is fully initialized but before serving traffic, which
  73 + * is exactly the right window: services are wired, the schema is
  74 + * applied, but the first HTTP request hasn't arrived yet.
  75 + *
  76 + * **Lives in distribution.** This is the only module that
  77 + * already depends on every PBC, which is what the seeder needs
  78 + * to compose. It's gated behind a property the production
  79 + * application.yaml never sets, so its presence in the fat-jar
  80 + * is dormant unless explicitly opted in.
  81 + */
  82 +@Component
  83 +@ConditionalOnProperty(prefix = "vibeerp.demo", name = ["seed"], havingValue = "true")
  84 +class DemoSeedRunner(
  85 + private val itemService: ItemService,
  86 + private val locationService: LocationService,
  87 + private val stockBalanceService: StockBalanceService,
  88 + private val partnerService: PartnerService,
  89 + private val salesOrderService: SalesOrderService,
  90 + private val purchaseOrderService: PurchaseOrderService,
  91 +) : CommandLineRunner {
  92 +
  93 + private val log = LoggerFactory.getLogger(DemoSeedRunner::class.java)
  94 +
  95 + @Transactional
  96 + override fun run(vararg args: String?) {
  97 + if (itemService.findByCode(SENTINEL_ITEM_CODE) != null) {
  98 + log.info("Demo seed: data already present (sentinel item {} found); skipping", SENTINEL_ITEM_CODE)
  99 + return
  100 + }
  101 +
  102 + log.info("Demo seed: populating starter dataset…")
  103 + PrincipalContext.runAs("__demo_seed__") {
  104 + seedItems()
  105 + seedLocations()
  106 + seedPartners()
  107 + seedStock()
  108 + seedSalesOrder()
  109 + seedPurchaseOrder()
  110 + }
  111 + log.info("Demo seed: done")
  112 + }
  113 +
  114 + // ─── Items ───────────────────────────────────────────────────────
  115 +
  116 + private fun seedItems() {
  117 + item(SENTINEL_ITEM_CODE, "A4 paper, 80gsm, white", ItemType.GOOD, "sheet")
  118 + item("DEMO-INK-CYAN", "Cyan offset ink", ItemType.GOOD, "kg")
  119 + item("DEMO-INK-MAGENTA", "Magenta offset ink", ItemType.GOOD, "kg")
  120 + item("DEMO-CARD-BIZ", "Business cards, 100/pack", ItemType.GOOD, "pack")
  121 + item("DEMO-BROCHURE-A5", "Folded A5 brochure", ItemType.GOOD, "ea")
  122 + }
  123 +
  124 + private fun item(code: String, name: String, type: ItemType, baseUomCode: String) {
  125 + itemService.create(
  126 + CreateItemCommand(
  127 + code = code,
  128 + name = name,
  129 + description = null,
  130 + itemType = type,
  131 + baseUomCode = baseUomCode,
  132 + active = true,
  133 + ),
  134 + )
  135 + }
  136 +
  137 + // ─── Locations ───────────────────────────────────────────────────
  138 +
  139 + private fun seedLocations() {
  140 + location("DEMO-WH-RAW", "Raw materials warehouse")
  141 + location("DEMO-WH-FG", "Finished goods warehouse")
  142 + }
  143 +
  144 + private fun location(code: String, name: String) {
  145 + locationService.create(
  146 + CreateLocationCommand(
  147 + code = code,
  148 + name = name,
  149 + type = LocationType.WAREHOUSE,
  150 + active = true,
  151 + ),
  152 + )
  153 + }
  154 +
  155 + // ─── Partners ────────────────────────────────────────────────────
  156 +
  157 + private fun seedPartners() {
  158 + partner("DEMO-CUST-ACME", "Acme Print Co.", PartnerType.CUSTOMER, "ap@acme.example")
  159 + partner("DEMO-CUST-GLOBE", "Globe Marketing", PartnerType.CUSTOMER, "ops@globe.example")
  160 + partner("DEMO-SUPP-PAPERWORLD", "Paper World Ltd.", PartnerType.SUPPLIER, "sales@paperworld.example")
  161 + partner("DEMO-SUPP-INKCO", "InkCo Industries", PartnerType.SUPPLIER, "orders@inkco.example")
  162 + }
  163 +
  164 + private fun partner(code: String, name: String, type: PartnerType, email: String) {
  165 + partnerService.create(
  166 + CreatePartnerCommand(
  167 + code = code,
  168 + name = name,
  169 + type = type,
  170 + email = email,
  171 + active = true,
  172 + ),
  173 + )
  174 + }
  175 +
  176 + // ─── Initial stock ───────────────────────────────────────────────
  177 +
  178 + private fun seedStock() {
  179 + val rawWh = locationService.findByCode("DEMO-WH-RAW")!!
  180 + val fgWh = locationService.findByCode("DEMO-WH-FG")!!
  181 +
  182 + stockBalanceService.adjust(SENTINEL_ITEM_CODE, rawWh.id, BigDecimal("5000"))
  183 + stockBalanceService.adjust("DEMO-INK-CYAN", rawWh.id, BigDecimal("50"))
  184 + stockBalanceService.adjust("DEMO-INK-MAGENTA", rawWh.id, BigDecimal("50"))
  185 +
  186 + stockBalanceService.adjust("DEMO-CARD-BIZ", fgWh.id, BigDecimal("200"))
  187 + stockBalanceService.adjust("DEMO-BROCHURE-A5", fgWh.id, BigDecimal("100"))
  188 + }
  189 +
  190 + // ─── Open sales order (DRAFT — ready to confirm + ship) ──────────
  191 +
  192 + private fun seedSalesOrder() {
  193 + salesOrderService.create(
  194 + CreateSalesOrderCommand(
  195 + code = "DEMO-SO-0001",
  196 + partnerCode = "DEMO-CUST-ACME",
  197 + orderDate = LocalDate.now(),
  198 + currencyCode = "USD",
  199 + lines = listOf(
  200 + SalesOrderLineCommand(
  201 + lineNo = 1,
  202 + itemCode = "DEMO-CARD-BIZ",
  203 + quantity = BigDecimal("50"),
  204 + unitPrice = BigDecimal("12.50"),
  205 + currencyCode = "USD",
  206 + ),
  207 + SalesOrderLineCommand(
  208 + lineNo = 2,
  209 + itemCode = "DEMO-BROCHURE-A5",
  210 + quantity = BigDecimal("20"),
  211 + unitPrice = BigDecimal("4.75"),
  212 + currencyCode = "USD",
  213 + ),
  214 + ),
  215 + ),
  216 + )
  217 + }
  218 +
  219 + // ─── Open purchase order (DRAFT — ready to confirm + receive) ────
  220 +
  221 + private fun seedPurchaseOrder() {
  222 + purchaseOrderService.create(
  223 + CreatePurchaseOrderCommand(
  224 + code = "DEMO-PO-0001",
  225 + partnerCode = "DEMO-SUPP-PAPERWORLD",
  226 + orderDate = LocalDate.now(),
  227 + expectedDate = LocalDate.now().plusDays(7),
  228 + currencyCode = "USD",
  229 + lines = listOf(
  230 + PurchaseOrderLineCommand(
  231 + lineNo = 1,
  232 + itemCode = SENTINEL_ITEM_CODE,
  233 + quantity = BigDecimal("10000"),
  234 + unitPrice = BigDecimal("0.04"),
  235 + currencyCode = "USD",
  236 + ),
  237 + ),
  238 + ),
  239 + )
  240 + }
  241 +
  242 + companion object {
  243 + /**
  244 + * The seeder uses the presence of this item as the
  245 + * idempotency marker — re-running the seeder against a
  246 + * Postgres that already contains it short-circuits. The
  247 + * choice of "the very first item the seeder creates" is
  248 + * deliberate: if the seed transaction commits at all, this
  249 + * row is in the DB; if it doesn't, nothing is.
  250 + */
  251 + const val SENTINEL_ITEM_CODE: String = "DEMO-PAPER-A4"
  252 + }
  253 +}
... ...
distribution/src/main/resources/application-dev.yaml
... ... @@ -21,6 +21,14 @@ vibeerp:
21 21 directory: ./plugins-dev
22 22 files:
23 23 local-path: ./files-dev
  24 + demo:
  25 + # Opt in to the one-shot DemoSeedRunner (in distribution/.../demo/)
  26 + # which stages a tiny but representative dataset on first boot.
  27 + # Idempotent — the runner checks for its own sentinel item before
  28 + # creating anything, so restarting the dev server is a no-op once
  29 + # the seed has been applied. Production deployments leave this
  30 + # property unset and the @ConditionalOnProperty bean never loads.
  31 + seed: true
24 32  
25 33 logging:
26 34 level:
... ...
gradle.properties
... ... @@ -19,6 +19,6 @@ kotlin.incremental=true
19 19 # api/api-v1/build.gradle.kts.
20 20 # Bump this line in lockstep with PROGRESS.md's "Latest version"
21 21 # row when shipping a new working surface.
22   -vibeerp.version=0.28.0-SNAPSHOT
  22 +vibeerp.version=0.29.0-SNAPSHOT
23 23 vibeerp.api.version=1.0.0-SNAPSHOT
24 24 vibeerp.group=org.vibeerp
... ...
platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/web/SpaController.kt 0 → 100644
  1 +package org.vibeerp.platform.bootstrap.web
  2 +
  3 +import org.springframework.stereotype.Controller
  4 +import org.springframework.web.bind.annotation.GetMapping
  5 +
  6 +/**
  7 + * Static SPA fallback controller.
  8 + *
  9 + * **The problem.** The vibe_erp Web SPA (the `:web` Gradle subproject)
  10 + * uses React Router's HTML5 history mode for client-side routing.
  11 + * When a user hits `http://host/sales-orders` directly — by typing
  12 + * the URL, refreshing the page, or clicking a deep link — Spring
  13 + * Boot's static resource handler tries to serve a file at
  14 + * `static/sales-orders` and returns 404 because no such file
  15 + * exists. The actual routing for `/sales-orders` lives inside the
  16 + * SPA's compiled JavaScript, so we need the server to return
  17 + * `static/index.html` for every SPA route and let React Router
  18 + * take over once the bundle has loaded.
  19 + *
  20 + * **Why an explicit list, not a catch-all.** Spring Security needs
  21 + * to permit the same paths the SPA uses, and a catch-all
  22 + * `@GetMapping` with a wildcard path matcher would also intercept
  23 + * legitimate 404s for typoed API URLs under the api-v1 prefix
  24 + * (turning them into "here's the SPA's 404 page" instead of an
  25 + * honest server-side 404). Listing the SPA's known top-level routes
  26 + * here keeps the seam explicit: adding a new sidebar item to
  27 + * AppLayout.tsx requires adding the route here. The list is small
  28 + * (12 entries) so the maintenance cost is trivial; if it ever
  29 + * passes ~25 entries we can revisit with a single regex matcher.
  30 + *
  31 + * **Forward, not redirect.** `forward:/index.html` is an internal
  32 + * RequestDispatcher.forward — the URL bar still shows
  33 + * `/sales-orders` and Spring Security still sees the original
  34 + * request path. Redirecting (`redirect:/index.html`) would lose
  35 + * the deep link entirely and break the back button.
  36 + *
  37 + * **The "/" entry.** Without it, hitting the root would also fall
  38 + * through to the static resource handler, which DOES correctly
  39 + * serve `static/index.html` for `/`. We list it explicitly anyway
  40 + * so the route table is self-explanatory.
  41 + *
  42 + * **API paths are NOT in this list.** Anything under the api-v1
  43 + * prefix matches a `@RestController` mapping registered higher up
  44 + * in the request-mapping chain, so the SPA fallback is never
  45 + * reached for an authentic API request. For an UNKNOWN API path,
  46 + * the absence of a controller match returns a real 404, which is
  47 + * what we want.
  48 + *
  49 + * **Where this controller is in the build.** Lives in
  50 + * `platform-bootstrap` rather than `:distribution` because it is
  51 + * a `@Controller` that needs to be picked up by Spring's component
  52 + * scan, and `platform-bootstrap` is the only module with the
  53 + * `@SpringBootApplication`. Putting it in `:distribution` would
  54 + * require adding a Spring scan to a module that has zero Kotlin
  55 + * sources of its own.
  56 + */
  57 +@Controller
  58 +class SpaController {
  59 +
  60 + @GetMapping(
  61 + value = [
  62 + // Root + auth
  63 + "/",
  64 + "/login",
  65 +
  66 + // Catalog & partners
  67 + "/items", "/items/", "/items/**",
  68 + "/uoms", "/uoms/", "/uoms/**",
  69 + "/partners", "/partners/", "/partners/**",
  70 +
  71 + // Inventory
  72 + "/locations", "/locations/", "/locations/**",
  73 + "/balances", "/balances/", "/balances/**",
  74 + "/movements", "/movements/", "/movements/**",
  75 +
  76 + // Orders
  77 + "/sales-orders", "/sales-orders/", "/sales-orders/**",
  78 + "/purchase-orders", "/purchase-orders/", "/purchase-orders/**",
  79 +
  80 + // Production
  81 + "/work-orders", "/work-orders/", "/work-orders/**",
  82 + "/shop-floor", "/shop-floor/", "/shop-floor/**",
  83 +
  84 + // Finance
  85 + "/journal-entries", "/journal-entries/", "/journal-entries/**",
  86 + ],
  87 + )
  88 + fun spa(): String = "forward:/index.html"
  89 +}
... ...
platform/platform-security/src/main/kotlin/org/vibeerp/platform/security/SecurityConfiguration.kt
... ... @@ -37,6 +37,15 @@ class SecurityConfiguration {
37 37 .cors { } // default; tighten in v0.4 once the React SPA exists
38 38 .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
39 39 .authorizeHttpRequests { auth ->
  40 + // ─── Public API endpoints ───────────────────────────
  41 + //
  42 + // The framework's API surface is split into a tiny
  43 + // public allowlist and a much larger authenticated
  44 + // surface. Listing the public bits explicitly first
  45 + // (and authenticating /api/** as a whole afterwards)
  46 + // keeps the matcher precedence honest: a typo in a
  47 + // public path can never accidentally permit an
  48 + // authenticated endpoint.
40 49 auth.requestMatchers(
41 50 // Public actuator probes
42 51 "/actuator/health",
... ... @@ -46,17 +55,65 @@ class SecurityConfiguration {
46 55 // Auth endpoints themselves cannot require auth
47 56 "/api/v1/auth/login",
48 57 "/api/v1/auth/refresh",
49   - // OpenAPI / Swagger UI — public because it describes the
50   - // API surface, not the data. The *data* still requires a
51   - // valid JWT: an unauthenticated "Try it out" call from
52   - // the Swagger UI against a pbc endpoint returns 401 just
53   - // like a curl does. Exposing the spec is the first step
54   - // toward both the future R1 web SPA (OpenAPI codegen)
55   - // and the A1 MCP server (discoverable tool catalog).
  58 + // OpenAPI / Swagger UI — public because it describes
  59 + // the API surface, not the data. The *data* still
  60 + // requires a valid JWT: an unauthenticated "Try it
  61 + // out" call against a pbc endpoint returns 401.
56 62 "/v3/api-docs/**",
57 63 "/swagger-ui/**",
58 64 "/swagger-ui.html",
59 65 ).permitAll()
  66 +
  67 + // Every other /api/** call requires a Bearer JWT.
  68 + // This MUST come before the SPA permitAll below; the
  69 + // permit-all rule for "/" + static assets uses
  70 + // anyRequest() and would otherwise swallow API calls
  71 + // too. The order preserves the framework's "API is
  72 + // always authenticated" invariant even as the SPA
  73 + // moves into the same fat-jar.
  74 + auth.requestMatchers("/api/**").authenticated()
  75 +
  76 + // ─── Web SPA (the :web React+TS bundle) ─────────────
  77 + //
  78 + // Static asset paths the Vite build produces.
  79 + // Permitted because the SPA itself is just HTML+CSS+JS
  80 + // — every secret is fetched from /api/** which is
  81 + // already gated above. A user without a valid JWT
  82 + // sees the login page instead of a forbidden screen,
  83 + // which is the correct UX.
  84 + auth.requestMatchers(
  85 + "/",
  86 + "/index.html",
  87 + "/favicon.svg",
  88 + "/favicon.ico",
  89 + "/manifest.json",
  90 + "/assets/**",
  91 + "/static/**",
  92 + ).permitAll()
  93 +
  94 + // SPA route paths handled by the SpaController in
  95 + // platform-bootstrap (which forwards them to
  96 + // /index.html so React Router can take over). The
  97 + // controller and this list MUST stay in sync — adding
  98 + // a sidebar entry to AppLayout.tsx implies updating
  99 + // both. See SpaController for the rationale.
  100 + auth.requestMatchers(
  101 + "/login",
  102 + "/items", "/items/**",
  103 + "/uoms", "/uoms/**",
  104 + "/partners", "/partners/**",
  105 + "/locations", "/locations/**",
  106 + "/balances", "/balances/**",
  107 + "/movements", "/movements/**",
  108 + "/sales-orders", "/sales-orders/**",
  109 + "/purchase-orders", "/purchase-orders/**",
  110 + "/work-orders", "/work-orders/**",
  111 + "/shop-floor", "/shop-floor/**",
  112 + "/journal-entries", "/journal-entries/**",
  113 + ).permitAll()
  114 +
  115 + // Anything else — return 401 so a typoed deep link
  116 + // doesn't accidentally serve the SPA shell.
60 117 auth.anyRequest().authenticated()
61 118 }
62 119 .oauth2ResourceServer { rs ->
... ...
settings.gradle.kts
... ... @@ -89,6 +89,16 @@ project(&quot;:pbc:pbc-production&quot;).projectDir = file(&quot;pbc/pbc-production&quot;)
89 89 include(":reference-customer:plugin-printing-shop")
90 90 project(":reference-customer:plugin-printing-shop").projectDir = file("reference-customer/plugin-printing-shop")
91 91  
  92 +// ─── Web SPA ────────────────────────────────────────────────────────
  93 +//
  94 +// `:web` is a Gradle wrapper around an npm build. It produces a
  95 +// `dist/` directory of static assets that `:distribution` consumes
  96 +// at processResources time. See `web/build.gradle.kts` for the
  97 +// rationale (no Kotlin/JVM source set, no node-gradle plugin —
  98 +// just Exec tasks against system npm).
  99 +include(":web")
  100 +project(":web").projectDir = file("web")
  101 +
92 102 // ─── Distribution (assembles the runnable image) ────────────────────
93 103 include(":distribution")
94 104 project(":distribution").projectDir = file("distribution")
... ...
web/.gitignore 0 → 100644
  1 +node_modules/
  2 +dist/
  3 +.vite/
  4 +*.log
  5 +.DS_Store
  6 +*.tsbuildinfo
  7 +# Compiled outputs from `tsc -b` for the project reference
  8 +# (vite.config.ts → vite.config.d.ts + vite.config.js).
  9 +vite.config.d.ts
  10 +vite.config.js
... ...
web/build.gradle.kts 0 → 100644
  1 +// vibe_erp web SPA — Gradle wrapper around the npm build.
  2 +//
  3 +// **Why no Kotlin/JVM source set.** This subproject has zero JVM
  4 +// code: every artifact under src/ is TypeScript/React. The Gradle
  5 +// presence is purely so `./gradlew build` runs the npm build as
  6 +// part of the standard build pipeline, and so :distribution can
  7 +// declare a normal project dependency on the produced static
  8 +// bundle instead of a fragile path reference.
  9 +//
  10 +// **Tasks.**
  11 +// - `npmInstall` → runs `npm install` in web/. Inputs:
  12 +// package.json + package-lock.json. Output: node_modules/.
  13 +// - `npmBuild` → runs `npm run build` in web/. Inputs:
  14 +// src/**, index.html, vite.config.ts, tsconfig*.json,
  15 +// tailwind.config.js, postcss.config.js. Output: dist/.
  16 +// - `assemble` → wired to depend on npmBuild so a normal
  17 +// `./gradlew :web:build` produces dist/.
  18 +//
  19 +// **No npm/node Gradle plugin.** I deliberately avoided
  20 +// `com.github.node-gradle.node` to keep the dep surface minimal —
  21 +// every plugin is one more thing to keep current with Gradle and
  22 +// JDK upgrades. Exec tasks are stable and the inputs/outputs
  23 +// declarations give Gradle the same up-to-date checking the plugin
  24 +// would.
  25 +//
  26 +// **Where the bundle goes.** Read by `:distribution` as a normal
  27 +// project artifact via the `webStaticBundle` configuration below;
  28 +// the consuming module copies dist/ into the Spring Boot
  29 +// classpath under `static/` at processResources time so the
  30 +// bootJar contains the entire SPA.
  31 +
  32 +import org.gradle.api.tasks.Exec
  33 +
  34 +plugins {
  35 + base
  36 +}
  37 +
  38 +description = "vibe_erp web SPA — Vite + React + TS, served by Spring Boot at the root path."
  39 +
  40 +// Resolve npm against the system PATH. Same approach as the
  41 +// reference plug-in's installToDev task — keeps developer setup
  42 +// simple ("you must have node + npm on PATH").
  43 +val npmCommand: String = if (org.gradle.internal.os.OperatingSystem.current().isWindows) "npm.cmd" else "npm"
  44 +
  45 +val npmInstall by tasks.registering(Exec::class) {
  46 + group = "build"
  47 + description = "Run `npm install` in web/."
  48 + workingDir = projectDir
  49 + commandLine(npmCommand, "install", "--no-audit", "--no-fund", "--silent")
  50 +
  51 + inputs.file("package.json")
  52 + inputs.file("package-lock.json").optional()
  53 + outputs.dir("node_modules")
  54 +}
  55 +
  56 +val npmBuild by tasks.registering(Exec::class) {
  57 + group = "build"
  58 + description = "Run `npm run build` in web/. Produces dist/."
  59 + workingDir = projectDir
  60 + commandLine(npmCommand, "run", "build")
  61 +
  62 + dependsOn(npmInstall)
  63 +
  64 + inputs.dir("src")
  65 + inputs.file("index.html")
  66 + inputs.file("vite.config.ts")
  67 + inputs.file("tsconfig.json")
  68 + inputs.file("tsconfig.node.json")
  69 + inputs.file("tailwind.config.js")
  70 + inputs.file("postcss.config.js")
  71 + inputs.file("package.json")
  72 +
  73 + outputs.dir("dist")
  74 +}
  75 +
  76 +tasks.named("assemble") {
  77 + dependsOn(npmBuild)
  78 +}
  79 +
  80 +// Outgoing configuration so :distribution can consume the dist
  81 +// directory as a normal Gradle artifact. Using a single-element
  82 +// `artifacts` block keeps the consumer side trivial.
  83 +val webStaticBundle: Configuration by configurations.creating {
  84 + isCanBeConsumed = true
  85 + isCanBeResolved = false
  86 +}
  87 +
  88 +artifacts {
  89 + add(webStaticBundle.name, file("dist")) {
  90 + builtBy(npmBuild)
  91 + }
  92 +}
... ...
web/index.html 0 → 100644
  1 +<!doctype html>
  2 +<html lang="en">
  3 + <head>
  4 + <meta charset="UTF-8" />
  5 + <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
  6 + <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7 + <meta name="description" content="vibe_erp — composable ERP framework for the printing industry" />
  8 + <title>vibe_erp</title>
  9 + </head>
  10 + <body class="bg-slate-50">
  11 + <div id="root"></div>
  12 + <script type="module" src="/src/main.tsx"></script>
  13 + </body>
  14 +</html>
... ...
web/package-lock.json 0 → 100644
  1 +{
  2 + "name": "vibe-erp-web",
  3 + "version": "0.0.0",
  4 + "lockfileVersion": 3,
  5 + "requires": true,
  6 + "packages": {
  7 + "": {
  8 + "name": "vibe-erp-web",
  9 + "version": "0.0.0",
  10 + "dependencies": {
  11 + "react": "^18.3.1",
  12 + "react-dom": "^18.3.1",
  13 + "react-router-dom": "^6.28.0"
  14 + },
  15 + "devDependencies": {
  16 + "@types/node": "^25.5.2",
  17 + "@types/react": "^18.3.12",
  18 + "@types/react-dom": "^18.3.1",
  19 + "@vitejs/plugin-react": "^4.3.3",
  20 + "autoprefixer": "^10.4.20",
  21 + "postcss": "^8.4.49",
  22 + "tailwindcss": "^3.4.15",
  23 + "typescript": "^5.6.3",
  24 + "vite": "^5.4.11"
  25 + }
  26 + },
  27 + "node_modules/@alloc/quick-lru": {
  28 + "version": "5.2.0",
  29 + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
  30 + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
  31 + "dev": true,
  32 + "license": "MIT",
  33 + "engines": {
  34 + "node": ">=10"
  35 + },
  36 + "funding": {
  37 + "url": "https://github.com/sponsors/sindresorhus"
  38 + }
  39 + },
  40 + "node_modules/@babel/code-frame": {
  41 + "version": "7.29.0",
  42 + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
  43 + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
  44 + "dev": true,
  45 + "license": "MIT",
  46 + "dependencies": {
  47 + "@babel/helper-validator-identifier": "^7.28.5",
  48 + "js-tokens": "^4.0.0",
  49 + "picocolors": "^1.1.1"
  50 + },
  51 + "engines": {
  52 + "node": ">=6.9.0"
  53 + }
  54 + },
  55 + "node_modules/@babel/compat-data": {
  56 + "version": "7.29.0",
  57 + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
  58 + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
  59 + "dev": true,
  60 + "license": "MIT",
  61 + "engines": {
  62 + "node": ">=6.9.0"
  63 + }
  64 + },
  65 + "node_modules/@babel/core": {
  66 + "version": "7.29.0",
  67 + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
  68 + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
  69 + "dev": true,
  70 + "license": "MIT",
  71 + "dependencies": {
  72 + "@babel/code-frame": "^7.29.0",
  73 + "@babel/generator": "^7.29.0",
  74 + "@babel/helper-compilation-targets": "^7.28.6",
  75 + "@babel/helper-module-transforms": "^7.28.6",
  76 + "@babel/helpers": "^7.28.6",
  77 + "@babel/parser": "^7.29.0",
  78 + "@babel/template": "^7.28.6",
  79 + "@babel/traverse": "^7.29.0",
  80 + "@babel/types": "^7.29.0",
  81 + "@jridgewell/remapping": "^2.3.5",
  82 + "convert-source-map": "^2.0.0",
  83 + "debug": "^4.1.0",
  84 + "gensync": "^1.0.0-beta.2",
  85 + "json5": "^2.2.3",
  86 + "semver": "^6.3.1"
  87 + },
  88 + "engines": {
  89 + "node": ">=6.9.0"
  90 + },
  91 + "funding": {
  92 + "type": "opencollective",
  93 + "url": "https://opencollective.com/babel"
  94 + }
  95 + },
  96 + "node_modules/@babel/generator": {
  97 + "version": "7.29.1",
  98 + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
  99 + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
  100 + "dev": true,
  101 + "license": "MIT",
  102 + "dependencies": {
  103 + "@babel/parser": "^7.29.0",
  104 + "@babel/types": "^7.29.0",
  105 + "@jridgewell/gen-mapping": "^0.3.12",
  106 + "@jridgewell/trace-mapping": "^0.3.28",
  107 + "jsesc": "^3.0.2"
  108 + },
  109 + "engines": {
  110 + "node": ">=6.9.0"
  111 + }
  112 + },
  113 + "node_modules/@babel/helper-compilation-targets": {
  114 + "version": "7.28.6",
  115 + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
  116 + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
  117 + "dev": true,
  118 + "license": "MIT",
  119 + "dependencies": {
  120 + "@babel/compat-data": "^7.28.6",
  121 + "@babel/helper-validator-option": "^7.27.1",
  122 + "browserslist": "^4.24.0",
  123 + "lru-cache": "^5.1.1",
  124 + "semver": "^6.3.1"
  125 + },
  126 + "engines": {
  127 + "node": ">=6.9.0"
  128 + }
  129 + },
  130 + "node_modules/@babel/helper-globals": {
  131 + "version": "7.28.0",
  132 + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
  133 + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
  134 + "dev": true,
  135 + "license": "MIT",
  136 + "engines": {
  137 + "node": ">=6.9.0"
  138 + }
  139 + },
  140 + "node_modules/@babel/helper-module-imports": {
  141 + "version": "7.28.6",
  142 + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
  143 + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
  144 + "dev": true,
  145 + "license": "MIT",
  146 + "dependencies": {
  147 + "@babel/traverse": "^7.28.6",
  148 + "@babel/types": "^7.28.6"
  149 + },
  150 + "engines": {
  151 + "node": ">=6.9.0"
  152 + }
  153 + },
  154 + "node_modules/@babel/helper-module-transforms": {
  155 + "version": "7.28.6",
  156 + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
  157 + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
  158 + "dev": true,
  159 + "license": "MIT",
  160 + "dependencies": {
  161 + "@babel/helper-module-imports": "^7.28.6",
  162 + "@babel/helper-validator-identifier": "^7.28.5",
  163 + "@babel/traverse": "^7.28.6"
  164 + },
  165 + "engines": {
  166 + "node": ">=6.9.0"
  167 + },
  168 + "peerDependencies": {
  169 + "@babel/core": "^7.0.0"
  170 + }
  171 + },
  172 + "node_modules/@babel/helper-plugin-utils": {
  173 + "version": "7.28.6",
  174 + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
  175 + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
  176 + "dev": true,
  177 + "license": "MIT",
  178 + "engines": {
  179 + "node": ">=6.9.0"
  180 + }
  181 + },
  182 + "node_modules/@babel/helper-string-parser": {
  183 + "version": "7.27.1",
  184 + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
  185 + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
  186 + "dev": true,
  187 + "license": "MIT",
  188 + "engines": {
  189 + "node": ">=6.9.0"
  190 + }
  191 + },
  192 + "node_modules/@babel/helper-validator-identifier": {
  193 + "version": "7.28.5",
  194 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
  195 + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
  196 + "dev": true,
  197 + "license": "MIT",
  198 + "engines": {
  199 + "node": ">=6.9.0"
  200 + }
  201 + },
  202 + "node_modules/@babel/helper-validator-option": {
  203 + "version": "7.27.1",
  204 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
  205 + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
  206 + "dev": true,
  207 + "license": "MIT",
  208 + "engines": {
  209 + "node": ">=6.9.0"
  210 + }
  211 + },
  212 + "node_modules/@babel/helpers": {
  213 + "version": "7.29.2",
  214 + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
  215 + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
  216 + "dev": true,
  217 + "license": "MIT",
  218 + "dependencies": {
  219 + "@babel/template": "^7.28.6",
  220 + "@babel/types": "^7.29.0"
  221 + },
  222 + "engines": {
  223 + "node": ">=6.9.0"
  224 + }
  225 + },
  226 + "node_modules/@babel/parser": {
  227 + "version": "7.29.2",
  228 + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
  229 + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
  230 + "dev": true,
  231 + "license": "MIT",
  232 + "dependencies": {
  233 + "@babel/types": "^7.29.0"
  234 + },
  235 + "bin": {
  236 + "parser": "bin/babel-parser.js"
  237 + },
  238 + "engines": {
  239 + "node": ">=6.0.0"
  240 + }
  241 + },
  242 + "node_modules/@babel/plugin-transform-react-jsx-self": {
  243 + "version": "7.27.1",
  244 + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
  245 + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
  246 + "dev": true,
  247 + "license": "MIT",
  248 + "dependencies": {
  249 + "@babel/helper-plugin-utils": "^7.27.1"
  250 + },
  251 + "engines": {
  252 + "node": ">=6.9.0"
  253 + },
  254 + "peerDependencies": {
  255 + "@babel/core": "^7.0.0-0"
  256 + }
  257 + },
  258 + "node_modules/@babel/plugin-transform-react-jsx-source": {
  259 + "version": "7.27.1",
  260 + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
  261 + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
  262 + "dev": true,
  263 + "license": "MIT",
  264 + "dependencies": {
  265 + "@babel/helper-plugin-utils": "^7.27.1"
  266 + },
  267 + "engines": {
  268 + "node": ">=6.9.0"
  269 + },
  270 + "peerDependencies": {
  271 + "@babel/core": "^7.0.0-0"
  272 + }
  273 + },
  274 + "node_modules/@babel/template": {
  275 + "version": "7.28.6",
  276 + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
  277 + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
  278 + "dev": true,
  279 + "license": "MIT",
  280 + "dependencies": {
  281 + "@babel/code-frame": "^7.28.6",
  282 + "@babel/parser": "^7.28.6",
  283 + "@babel/types": "^7.28.6"
  284 + },
  285 + "engines": {
  286 + "node": ">=6.9.0"
  287 + }
  288 + },
  289 + "node_modules/@babel/traverse": {
  290 + "version": "7.29.0",
  291 + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
  292 + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
  293 + "dev": true,
  294 + "license": "MIT",
  295 + "dependencies": {
  296 + "@babel/code-frame": "^7.29.0",
  297 + "@babel/generator": "^7.29.0",
  298 + "@babel/helper-globals": "^7.28.0",
  299 + "@babel/parser": "^7.29.0",
  300 + "@babel/template": "^7.28.6",
  301 + "@babel/types": "^7.29.0",
  302 + "debug": "^4.3.1"
  303 + },
  304 + "engines": {
  305 + "node": ">=6.9.0"
  306 + }
  307 + },
  308 + "node_modules/@babel/types": {
  309 + "version": "7.29.0",
  310 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
  311 + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
  312 + "dev": true,
  313 + "license": "MIT",
  314 + "dependencies": {
  315 + "@babel/helper-string-parser": "^7.27.1",
  316 + "@babel/helper-validator-identifier": "^7.28.5"
  317 + },
  318 + "engines": {
  319 + "node": ">=6.9.0"
  320 + }
  321 + },
  322 + "node_modules/@esbuild/aix-ppc64": {
  323 + "version": "0.21.5",
  324 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
  325 + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
  326 + "cpu": [
  327 + "ppc64"
  328 + ],
  329 + "dev": true,
  330 + "license": "MIT",
  331 + "optional": true,
  332 + "os": [
  333 + "aix"
  334 + ],
  335 + "engines": {
  336 + "node": ">=12"
  337 + }
  338 + },
  339 + "node_modules/@esbuild/android-arm": {
  340 + "version": "0.21.5",
  341 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
  342 + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
  343 + "cpu": [
  344 + "arm"
  345 + ],
  346 + "dev": true,
  347 + "license": "MIT",
  348 + "optional": true,
  349 + "os": [
  350 + "android"
  351 + ],
  352 + "engines": {
  353 + "node": ">=12"
  354 + }
  355 + },
  356 + "node_modules/@esbuild/android-arm64": {
  357 + "version": "0.21.5",
  358 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
  359 + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
  360 + "cpu": [
  361 + "arm64"
  362 + ],
  363 + "dev": true,
  364 + "license": "MIT",
  365 + "optional": true,
  366 + "os": [
  367 + "android"
  368 + ],
  369 + "engines": {
  370 + "node": ">=12"
  371 + }
  372 + },
  373 + "node_modules/@esbuild/android-x64": {
  374 + "version": "0.21.5",
  375 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
  376 + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
  377 + "cpu": [
  378 + "x64"
  379 + ],
  380 + "dev": true,
  381 + "license": "MIT",
  382 + "optional": true,
  383 + "os": [
  384 + "android"
  385 + ],
  386 + "engines": {
  387 + "node": ">=12"
  388 + }
  389 + },
  390 + "node_modules/@esbuild/darwin-arm64": {
  391 + "version": "0.21.5",
  392 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
  393 + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
  394 + "cpu": [
  395 + "arm64"
  396 + ],
  397 + "dev": true,
  398 + "license": "MIT",
  399 + "optional": true,
  400 + "os": [
  401 + "darwin"
  402 + ],
  403 + "engines": {
  404 + "node": ">=12"
  405 + }
  406 + },
  407 + "node_modules/@esbuild/darwin-x64": {
  408 + "version": "0.21.5",
  409 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
  410 + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
  411 + "cpu": [
  412 + "x64"
  413 + ],
  414 + "dev": true,
  415 + "license": "MIT",
  416 + "optional": true,
  417 + "os": [
  418 + "darwin"
  419 + ],
  420 + "engines": {
  421 + "node": ">=12"
  422 + }
  423 + },
  424 + "node_modules/@esbuild/freebsd-arm64": {
  425 + "version": "0.21.5",
  426 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
  427 + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
  428 + "cpu": [
  429 + "arm64"
  430 + ],
  431 + "dev": true,
  432 + "license": "MIT",
  433 + "optional": true,
  434 + "os": [
  435 + "freebsd"
  436 + ],
  437 + "engines": {
  438 + "node": ">=12"
  439 + }
  440 + },
  441 + "node_modules/@esbuild/freebsd-x64": {
  442 + "version": "0.21.5",
  443 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
  444 + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
  445 + "cpu": [
  446 + "x64"
  447 + ],
  448 + "dev": true,
  449 + "license": "MIT",
  450 + "optional": true,
  451 + "os": [
  452 + "freebsd"
  453 + ],
  454 + "engines": {
  455 + "node": ">=12"
  456 + }
  457 + },
  458 + "node_modules/@esbuild/linux-arm": {
  459 + "version": "0.21.5",
  460 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
  461 + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
  462 + "cpu": [
  463 + "arm"
  464 + ],
  465 + "dev": true,
  466 + "license": "MIT",
  467 + "optional": true,
  468 + "os": [
  469 + "linux"
  470 + ],
  471 + "engines": {
  472 + "node": ">=12"
  473 + }
  474 + },
  475 + "node_modules/@esbuild/linux-arm64": {
  476 + "version": "0.21.5",
  477 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
  478 + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
  479 + "cpu": [
  480 + "arm64"
  481 + ],
  482 + "dev": true,
  483 + "license": "MIT",
  484 + "optional": true,
  485 + "os": [
  486 + "linux"
  487 + ],
  488 + "engines": {
  489 + "node": ">=12"
  490 + }
  491 + },
  492 + "node_modules/@esbuild/linux-ia32": {
  493 + "version": "0.21.5",
  494 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
  495 + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
  496 + "cpu": [
  497 + "ia32"
  498 + ],
  499 + "dev": true,
  500 + "license": "MIT",
  501 + "optional": true,
  502 + "os": [
  503 + "linux"
  504 + ],
  505 + "engines": {
  506 + "node": ">=12"
  507 + }
  508 + },
  509 + "node_modules/@esbuild/linux-loong64": {
  510 + "version": "0.21.5",
  511 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
  512 + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
  513 + "cpu": [
  514 + "loong64"
  515 + ],
  516 + "dev": true,
  517 + "license": "MIT",
  518 + "optional": true,
  519 + "os": [
  520 + "linux"
  521 + ],
  522 + "engines": {
  523 + "node": ">=12"
  524 + }
  525 + },
  526 + "node_modules/@esbuild/linux-mips64el": {
  527 + "version": "0.21.5",
  528 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
  529 + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
  530 + "cpu": [
  531 + "mips64el"
  532 + ],
  533 + "dev": true,
  534 + "license": "MIT",
  535 + "optional": true,
  536 + "os": [
  537 + "linux"
  538 + ],
  539 + "engines": {
  540 + "node": ">=12"
  541 + }
  542 + },
  543 + "node_modules/@esbuild/linux-ppc64": {
  544 + "version": "0.21.5",
  545 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
  546 + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
  547 + "cpu": [
  548 + "ppc64"
  549 + ],
  550 + "dev": true,
  551 + "license": "MIT",
  552 + "optional": true,
  553 + "os": [
  554 + "linux"
  555 + ],
  556 + "engines": {
  557 + "node": ">=12"
  558 + }
  559 + },
  560 + "node_modules/@esbuild/linux-riscv64": {
  561 + "version": "0.21.5",
  562 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
  563 + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
  564 + "cpu": [
  565 + "riscv64"
  566 + ],
  567 + "dev": true,
  568 + "license": "MIT",
  569 + "optional": true,
  570 + "os": [
  571 + "linux"
  572 + ],
  573 + "engines": {
  574 + "node": ">=12"
  575 + }
  576 + },
  577 + "node_modules/@esbuild/linux-s390x": {
  578 + "version": "0.21.5",
  579 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
  580 + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
  581 + "cpu": [
  582 + "s390x"
  583 + ],
  584 + "dev": true,
  585 + "license": "MIT",
  586 + "optional": true,
  587 + "os": [
  588 + "linux"
  589 + ],
  590 + "engines": {
  591 + "node": ">=12"
  592 + }
  593 + },
  594 + "node_modules/@esbuild/linux-x64": {
  595 + "version": "0.21.5",
  596 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
  597 + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
  598 + "cpu": [
  599 + "x64"
  600 + ],
  601 + "dev": true,
  602 + "license": "MIT",
  603 + "optional": true,
  604 + "os": [
  605 + "linux"
  606 + ],
  607 + "engines": {
  608 + "node": ">=12"
  609 + }
  610 + },
  611 + "node_modules/@esbuild/netbsd-x64": {
  612 + "version": "0.21.5",
  613 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
  614 + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
  615 + "cpu": [
  616 + "x64"
  617 + ],
  618 + "dev": true,
  619 + "license": "MIT",
  620 + "optional": true,
  621 + "os": [
  622 + "netbsd"
  623 + ],
  624 + "engines": {
  625 + "node": ">=12"
  626 + }
  627 + },
  628 + "node_modules/@esbuild/openbsd-x64": {
  629 + "version": "0.21.5",
  630 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
  631 + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
  632 + "cpu": [
  633 + "x64"
  634 + ],
  635 + "dev": true,
  636 + "license": "MIT",
  637 + "optional": true,
  638 + "os": [
  639 + "openbsd"
  640 + ],
  641 + "engines": {
  642 + "node": ">=12"
  643 + }
  644 + },
  645 + "node_modules/@esbuild/sunos-x64": {
  646 + "version": "0.21.5",
  647 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
  648 + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
  649 + "cpu": [
  650 + "x64"
  651 + ],
  652 + "dev": true,
  653 + "license": "MIT",
  654 + "optional": true,
  655 + "os": [
  656 + "sunos"
  657 + ],
  658 + "engines": {
  659 + "node": ">=12"
  660 + }
  661 + },
  662 + "node_modules/@esbuild/win32-arm64": {
  663 + "version": "0.21.5",
  664 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
  665 + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
  666 + "cpu": [
  667 + "arm64"
  668 + ],
  669 + "dev": true,
  670 + "license": "MIT",
  671 + "optional": true,
  672 + "os": [
  673 + "win32"
  674 + ],
  675 + "engines": {
  676 + "node": ">=12"
  677 + }
  678 + },
  679 + "node_modules/@esbuild/win32-ia32": {
  680 + "version": "0.21.5",
  681 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
  682 + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
  683 + "cpu": [
  684 + "ia32"
  685 + ],
  686 + "dev": true,
  687 + "license": "MIT",
  688 + "optional": true,
  689 + "os": [
  690 + "win32"
  691 + ],
  692 + "engines": {
  693 + "node": ">=12"
  694 + }
  695 + },
  696 + "node_modules/@esbuild/win32-x64": {
  697 + "version": "0.21.5",
  698 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
  699 + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
  700 + "cpu": [
  701 + "x64"
  702 + ],
  703 + "dev": true,
  704 + "license": "MIT",
  705 + "optional": true,
  706 + "os": [
  707 + "win32"
  708 + ],
  709 + "engines": {
  710 + "node": ">=12"
  711 + }
  712 + },
  713 + "node_modules/@jridgewell/gen-mapping": {
  714 + "version": "0.3.13",
  715 + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
  716 + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
  717 + "dev": true,
  718 + "license": "MIT",
  719 + "dependencies": {
  720 + "@jridgewell/sourcemap-codec": "^1.5.0",
  721 + "@jridgewell/trace-mapping": "^0.3.24"
  722 + }
  723 + },
  724 + "node_modules/@jridgewell/remapping": {
  725 + "version": "2.3.5",
  726 + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
  727 + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
  728 + "dev": true,
  729 + "license": "MIT",
  730 + "dependencies": {
  731 + "@jridgewell/gen-mapping": "^0.3.5",
  732 + "@jridgewell/trace-mapping": "^0.3.24"
  733 + }
  734 + },
  735 + "node_modules/@jridgewell/resolve-uri": {
  736 + "version": "3.1.2",
  737 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
  738 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
  739 + "dev": true,
  740 + "license": "MIT",
  741 + "engines": {
  742 + "node": ">=6.0.0"
  743 + }
  744 + },
  745 + "node_modules/@jridgewell/sourcemap-codec": {
  746 + "version": "1.5.5",
  747 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
  748 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
  749 + "dev": true,
  750 + "license": "MIT"
  751 + },
  752 + "node_modules/@jridgewell/trace-mapping": {
  753 + "version": "0.3.31",
  754 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
  755 + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
  756 + "dev": true,
  757 + "license": "MIT",
  758 + "dependencies": {
  759 + "@jridgewell/resolve-uri": "^3.1.0",
  760 + "@jridgewell/sourcemap-codec": "^1.4.14"
  761 + }
  762 + },
  763 + "node_modules/@nodelib/fs.scandir": {
  764 + "version": "2.1.5",
  765 + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
  766 + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
  767 + "dev": true,
  768 + "license": "MIT",
  769 + "dependencies": {
  770 + "@nodelib/fs.stat": "2.0.5",
  771 + "run-parallel": "^1.1.9"
  772 + },
  773 + "engines": {
  774 + "node": ">= 8"
  775 + }
  776 + },
  777 + "node_modules/@nodelib/fs.stat": {
  778 + "version": "2.0.5",
  779 + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
  780 + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
  781 + "dev": true,
  782 + "license": "MIT",
  783 + "engines": {
  784 + "node": ">= 8"
  785 + }
  786 + },
  787 + "node_modules/@nodelib/fs.walk": {
  788 + "version": "1.2.8",
  789 + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
  790 + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
  791 + "dev": true,
  792 + "license": "MIT",
  793 + "dependencies": {
  794 + "@nodelib/fs.scandir": "2.1.5",
  795 + "fastq": "^1.6.0"
  796 + },
  797 + "engines": {
  798 + "node": ">= 8"
  799 + }
  800 + },
  801 + "node_modules/@remix-run/router": {
  802 + "version": "1.23.2",
  803 + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
  804 + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
  805 + "license": "MIT",
  806 + "engines": {
  807 + "node": ">=14.0.0"
  808 + }
  809 + },
  810 + "node_modules/@rolldown/pluginutils": {
  811 + "version": "1.0.0-beta.27",
  812 + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
  813 + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
  814 + "dev": true,
  815 + "license": "MIT"
  816 + },
  817 + "node_modules/@rollup/rollup-android-arm-eabi": {
  818 + "version": "4.60.1",
  819 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
  820 + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
  821 + "cpu": [
  822 + "arm"
  823 + ],
  824 + "dev": true,
  825 + "license": "MIT",
  826 + "optional": true,
  827 + "os": [
  828 + "android"
  829 + ]
  830 + },
  831 + "node_modules/@rollup/rollup-android-arm64": {
  832 + "version": "4.60.1",
  833 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
  834 + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
  835 + "cpu": [
  836 + "arm64"
  837 + ],
  838 + "dev": true,
  839 + "license": "MIT",
  840 + "optional": true,
  841 + "os": [
  842 + "android"
  843 + ]
  844 + },
  845 + "node_modules/@rollup/rollup-darwin-arm64": {
  846 + "version": "4.60.1",
  847 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
  848 + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
  849 + "cpu": [
  850 + "arm64"
  851 + ],
  852 + "dev": true,
  853 + "license": "MIT",
  854 + "optional": true,
  855 + "os": [
  856 + "darwin"
  857 + ]
  858 + },
  859 + "node_modules/@rollup/rollup-darwin-x64": {
  860 + "version": "4.60.1",
  861 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
  862 + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
  863 + "cpu": [
  864 + "x64"
  865 + ],
  866 + "dev": true,
  867 + "license": "MIT",
  868 + "optional": true,
  869 + "os": [
  870 + "darwin"
  871 + ]
  872 + },
  873 + "node_modules/@rollup/rollup-freebsd-arm64": {
  874 + "version": "4.60.1",
  875 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
  876 + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
  877 + "cpu": [
  878 + "arm64"
  879 + ],
  880 + "dev": true,
  881 + "license": "MIT",
  882 + "optional": true,
  883 + "os": [
  884 + "freebsd"
  885 + ]
  886 + },
  887 + "node_modules/@rollup/rollup-freebsd-x64": {
  888 + "version": "4.60.1",
  889 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
  890 + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
  891 + "cpu": [
  892 + "x64"
  893 + ],
  894 + "dev": true,
  895 + "license": "MIT",
  896 + "optional": true,
  897 + "os": [
  898 + "freebsd"
  899 + ]
  900 + },
  901 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
  902 + "version": "4.60.1",
  903 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
  904 + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
  905 + "cpu": [
  906 + "arm"
  907 + ],
  908 + "dev": true,
  909 + "libc": [
  910 + "glibc"
  911 + ],
  912 + "license": "MIT",
  913 + "optional": true,
  914 + "os": [
  915 + "linux"
  916 + ]
  917 + },
  918 + "node_modules/@rollup/rollup-linux-arm-musleabihf": {
  919 + "version": "4.60.1",
  920 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
  921 + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
  922 + "cpu": [
  923 + "arm"
  924 + ],
  925 + "dev": true,
  926 + "libc": [
  927 + "musl"
  928 + ],
  929 + "license": "MIT",
  930 + "optional": true,
  931 + "os": [
  932 + "linux"
  933 + ]
  934 + },
  935 + "node_modules/@rollup/rollup-linux-arm64-gnu": {
  936 + "version": "4.60.1",
  937 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
  938 + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
  939 + "cpu": [
  940 + "arm64"
  941 + ],
  942 + "dev": true,
  943 + "libc": [
  944 + "glibc"
  945 + ],
  946 + "license": "MIT",
  947 + "optional": true,
  948 + "os": [
  949 + "linux"
  950 + ]
  951 + },
  952 + "node_modules/@rollup/rollup-linux-arm64-musl": {
  953 + "version": "4.60.1",
  954 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
  955 + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
  956 + "cpu": [
  957 + "arm64"
  958 + ],
  959 + "dev": true,
  960 + "libc": [
  961 + "musl"
  962 + ],
  963 + "license": "MIT",
  964 + "optional": true,
  965 + "os": [
  966 + "linux"
  967 + ]
  968 + },
  969 + "node_modules/@rollup/rollup-linux-loong64-gnu": {
  970 + "version": "4.60.1",
  971 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
  972 + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
  973 + "cpu": [
  974 + "loong64"
  975 + ],
  976 + "dev": true,
  977 + "libc": [
  978 + "glibc"
  979 + ],
  980 + "license": "MIT",
  981 + "optional": true,
  982 + "os": [
  983 + "linux"
  984 + ]
  985 + },
  986 + "node_modules/@rollup/rollup-linux-loong64-musl": {
  987 + "version": "4.60.1",
  988 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
  989 + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
  990 + "cpu": [
  991 + "loong64"
  992 + ],
  993 + "dev": true,
  994 + "libc": [
  995 + "musl"
  996 + ],
  997 + "license": "MIT",
  998 + "optional": true,
  999 + "os": [
  1000 + "linux"
  1001 + ]
  1002 + },
  1003 + "node_modules/@rollup/rollup-linux-ppc64-gnu": {
  1004 + "version": "4.60.1",
  1005 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
  1006 + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
  1007 + "cpu": [
  1008 + "ppc64"
  1009 + ],
  1010 + "dev": true,
  1011 + "libc": [
  1012 + "glibc"
  1013 + ],
  1014 + "license": "MIT",
  1015 + "optional": true,
  1016 + "os": [
  1017 + "linux"
  1018 + ]
  1019 + },
  1020 + "node_modules/@rollup/rollup-linux-ppc64-musl": {
  1021 + "version": "4.60.1",
  1022 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
  1023 + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
  1024 + "cpu": [
  1025 + "ppc64"
  1026 + ],
  1027 + "dev": true,
  1028 + "libc": [
  1029 + "musl"
  1030 + ],
  1031 + "license": "MIT",
  1032 + "optional": true,
  1033 + "os": [
  1034 + "linux"
  1035 + ]
  1036 + },
  1037 + "node_modules/@rollup/rollup-linux-riscv64-gnu": {
  1038 + "version": "4.60.1",
  1039 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
  1040 + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
  1041 + "cpu": [
  1042 + "riscv64"
  1043 + ],
  1044 + "dev": true,
  1045 + "libc": [
  1046 + "glibc"
  1047 + ],
  1048 + "license": "MIT",
  1049 + "optional": true,
  1050 + "os": [
  1051 + "linux"
  1052 + ]
  1053 + },
  1054 + "node_modules/@rollup/rollup-linux-riscv64-musl": {
  1055 + "version": "4.60.1",
  1056 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
  1057 + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
  1058 + "cpu": [
  1059 + "riscv64"
  1060 + ],
  1061 + "dev": true,
  1062 + "libc": [
  1063 + "musl"
  1064 + ],
  1065 + "license": "MIT",
  1066 + "optional": true,
  1067 + "os": [
  1068 + "linux"
  1069 + ]
  1070 + },
  1071 + "node_modules/@rollup/rollup-linux-s390x-gnu": {
  1072 + "version": "4.60.1",
  1073 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
  1074 + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
  1075 + "cpu": [
  1076 + "s390x"
  1077 + ],
  1078 + "dev": true,
  1079 + "libc": [
  1080 + "glibc"
  1081 + ],
  1082 + "license": "MIT",
  1083 + "optional": true,
  1084 + "os": [
  1085 + "linux"
  1086 + ]
  1087 + },
  1088 + "node_modules/@rollup/rollup-linux-x64-gnu": {
  1089 + "version": "4.60.1",
  1090 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
  1091 + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
  1092 + "cpu": [
  1093 + "x64"
  1094 + ],
  1095 + "dev": true,
  1096 + "libc": [
  1097 + "glibc"
  1098 + ],
  1099 + "license": "MIT",
  1100 + "optional": true,
  1101 + "os": [
  1102 + "linux"
  1103 + ]
  1104 + },
  1105 + "node_modules/@rollup/rollup-linux-x64-musl": {
  1106 + "version": "4.60.1",
  1107 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
  1108 + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
  1109 + "cpu": [
  1110 + "x64"
  1111 + ],
  1112 + "dev": true,
  1113 + "libc": [
  1114 + "musl"
  1115 + ],
  1116 + "license": "MIT",
  1117 + "optional": true,
  1118 + "os": [
  1119 + "linux"
  1120 + ]
  1121 + },
  1122 + "node_modules/@rollup/rollup-openbsd-x64": {
  1123 + "version": "4.60.1",
  1124 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
  1125 + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
  1126 + "cpu": [
  1127 + "x64"
  1128 + ],
  1129 + "dev": true,
  1130 + "license": "MIT",
  1131 + "optional": true,
  1132 + "os": [
  1133 + "openbsd"
  1134 + ]
  1135 + },
  1136 + "node_modules/@rollup/rollup-openharmony-arm64": {
  1137 + "version": "4.60.1",
  1138 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
  1139 + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
  1140 + "cpu": [
  1141 + "arm64"
  1142 + ],
  1143 + "dev": true,
  1144 + "license": "MIT",
  1145 + "optional": true,
  1146 + "os": [
  1147 + "openharmony"
  1148 + ]
  1149 + },
  1150 + "node_modules/@rollup/rollup-win32-arm64-msvc": {
  1151 + "version": "4.60.1",
  1152 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
  1153 + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
  1154 + "cpu": [
  1155 + "arm64"
  1156 + ],
  1157 + "dev": true,
  1158 + "license": "MIT",
  1159 + "optional": true,
  1160 + "os": [
  1161 + "win32"
  1162 + ]
  1163 + },
  1164 + "node_modules/@rollup/rollup-win32-ia32-msvc": {
  1165 + "version": "4.60.1",
  1166 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
  1167 + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
  1168 + "cpu": [
  1169 + "ia32"
  1170 + ],
  1171 + "dev": true,
  1172 + "license": "MIT",
  1173 + "optional": true,
  1174 + "os": [
  1175 + "win32"
  1176 + ]
  1177 + },
  1178 + "node_modules/@rollup/rollup-win32-x64-gnu": {
  1179 + "version": "4.60.1",
  1180 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
  1181 + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
  1182 + "cpu": [
  1183 + "x64"
  1184 + ],
  1185 + "dev": true,
  1186 + "license": "MIT",
  1187 + "optional": true,
  1188 + "os": [
  1189 + "win32"
  1190 + ]
  1191 + },
  1192 + "node_modules/@rollup/rollup-win32-x64-msvc": {
  1193 + "version": "4.60.1",
  1194 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
  1195 + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
  1196 + "cpu": [
  1197 + "x64"
  1198 + ],
  1199 + "dev": true,
  1200 + "license": "MIT",
  1201 + "optional": true,
  1202 + "os": [
  1203 + "win32"
  1204 + ]
  1205 + },
  1206 + "node_modules/@types/babel__core": {
  1207 + "version": "7.20.5",
  1208 + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
  1209 + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
  1210 + "dev": true,
  1211 + "license": "MIT",
  1212 + "dependencies": {
  1213 + "@babel/parser": "^7.20.7",
  1214 + "@babel/types": "^7.20.7",
  1215 + "@types/babel__generator": "*",
  1216 + "@types/babel__template": "*",
  1217 + "@types/babel__traverse": "*"
  1218 + }
  1219 + },
  1220 + "node_modules/@types/babel__generator": {
  1221 + "version": "7.27.0",
  1222 + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
  1223 + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
  1224 + "dev": true,
  1225 + "license": "MIT",
  1226 + "dependencies": {
  1227 + "@babel/types": "^7.0.0"
  1228 + }
  1229 + },
  1230 + "node_modules/@types/babel__template": {
  1231 + "version": "7.4.4",
  1232 + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
  1233 + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
  1234 + "dev": true,
  1235 + "license": "MIT",
  1236 + "dependencies": {
  1237 + "@babel/parser": "^7.1.0",
  1238 + "@babel/types": "^7.0.0"
  1239 + }
  1240 + },
  1241 + "node_modules/@types/babel__traverse": {
  1242 + "version": "7.28.0",
  1243 + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
  1244 + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
  1245 + "dev": true,
  1246 + "license": "MIT",
  1247 + "dependencies": {
  1248 + "@babel/types": "^7.28.2"
  1249 + }
  1250 + },
  1251 + "node_modules/@types/estree": {
  1252 + "version": "1.0.8",
  1253 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
  1254 + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
  1255 + "dev": true,
  1256 + "license": "MIT"
  1257 + },
  1258 + "node_modules/@types/node": {
  1259 + "version": "25.5.2",
  1260 + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz",
  1261 + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==",
  1262 + "dev": true,
  1263 + "license": "MIT",
  1264 + "dependencies": {
  1265 + "undici-types": "~7.18.0"
  1266 + }
  1267 + },
  1268 + "node_modules/@types/prop-types": {
  1269 + "version": "15.7.15",
  1270 + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
  1271 + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
  1272 + "dev": true,
  1273 + "license": "MIT"
  1274 + },
  1275 + "node_modules/@types/react": {
  1276 + "version": "18.3.28",
  1277 + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
  1278 + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
  1279 + "dev": true,
  1280 + "license": "MIT",
  1281 + "dependencies": {
  1282 + "@types/prop-types": "*",
  1283 + "csstype": "^3.2.2"
  1284 + }
  1285 + },
  1286 + "node_modules/@types/react-dom": {
  1287 + "version": "18.3.7",
  1288 + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
  1289 + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
  1290 + "dev": true,
  1291 + "license": "MIT",
  1292 + "peerDependencies": {
  1293 + "@types/react": "^18.0.0"
  1294 + }
  1295 + },
  1296 + "node_modules/@vitejs/plugin-react": {
  1297 + "version": "4.7.0",
  1298 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
  1299 + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
  1300 + "dev": true,
  1301 + "license": "MIT",
  1302 + "dependencies": {
  1303 + "@babel/core": "^7.28.0",
  1304 + "@babel/plugin-transform-react-jsx-self": "^7.27.1",
  1305 + "@babel/plugin-transform-react-jsx-source": "^7.27.1",
  1306 + "@rolldown/pluginutils": "1.0.0-beta.27",
  1307 + "@types/babel__core": "^7.20.5",
  1308 + "react-refresh": "^0.17.0"
  1309 + },
  1310 + "engines": {
  1311 + "node": "^14.18.0 || >=16.0.0"
  1312 + },
  1313 + "peerDependencies": {
  1314 + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
  1315 + }
  1316 + },
  1317 + "node_modules/any-promise": {
  1318 + "version": "1.3.0",
  1319 + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
  1320 + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
  1321 + "dev": true,
  1322 + "license": "MIT"
  1323 + },
  1324 + "node_modules/anymatch": {
  1325 + "version": "3.1.3",
  1326 + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
  1327 + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
  1328 + "dev": true,
  1329 + "license": "ISC",
  1330 + "dependencies": {
  1331 + "normalize-path": "^3.0.0",
  1332 + "picomatch": "^2.0.4"
  1333 + },
  1334 + "engines": {
  1335 + "node": ">= 8"
  1336 + }
  1337 + },
  1338 + "node_modules/arg": {
  1339 + "version": "5.0.2",
  1340 + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
  1341 + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
  1342 + "dev": true,
  1343 + "license": "MIT"
  1344 + },
  1345 + "node_modules/autoprefixer": {
  1346 + "version": "10.4.27",
  1347 + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz",
  1348 + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==",
  1349 + "dev": true,
  1350 + "funding": [
  1351 + {
  1352 + "type": "opencollective",
  1353 + "url": "https://opencollective.com/postcss/"
  1354 + },
  1355 + {
  1356 + "type": "tidelift",
  1357 + "url": "https://tidelift.com/funding/github/npm/autoprefixer"
  1358 + },
  1359 + {
  1360 + "type": "github",
  1361 + "url": "https://github.com/sponsors/ai"
  1362 + }
  1363 + ],
  1364 + "license": "MIT",
  1365 + "dependencies": {
  1366 + "browserslist": "^4.28.1",
  1367 + "caniuse-lite": "^1.0.30001774",
  1368 + "fraction.js": "^5.3.4",
  1369 + "picocolors": "^1.1.1",
  1370 + "postcss-value-parser": "^4.2.0"
  1371 + },
  1372 + "bin": {
  1373 + "autoprefixer": "bin/autoprefixer"
  1374 + },
  1375 + "engines": {
  1376 + "node": "^10 || ^12 || >=14"
  1377 + },
  1378 + "peerDependencies": {
  1379 + "postcss": "^8.1.0"
  1380 + }
  1381 + },
  1382 + "node_modules/baseline-browser-mapping": {
  1383 + "version": "2.10.16",
  1384 + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz",
  1385 + "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==",
  1386 + "dev": true,
  1387 + "license": "Apache-2.0",
  1388 + "bin": {
  1389 + "baseline-browser-mapping": "dist/cli.cjs"
  1390 + },
  1391 + "engines": {
  1392 + "node": ">=6.0.0"
  1393 + }
  1394 + },
  1395 + "node_modules/binary-extensions": {
  1396 + "version": "2.3.0",
  1397 + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
  1398 + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
  1399 + "dev": true,
  1400 + "license": "MIT",
  1401 + "engines": {
  1402 + "node": ">=8"
  1403 + },
  1404 + "funding": {
  1405 + "url": "https://github.com/sponsors/sindresorhus"
  1406 + }
  1407 + },
  1408 + "node_modules/braces": {
  1409 + "version": "3.0.3",
  1410 + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
  1411 + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
  1412 + "dev": true,
  1413 + "license": "MIT",
  1414 + "dependencies": {
  1415 + "fill-range": "^7.1.1"
  1416 + },
  1417 + "engines": {
  1418 + "node": ">=8"
  1419 + }
  1420 + },
  1421 + "node_modules/browserslist": {
  1422 + "version": "4.28.2",
  1423 + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
  1424 + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
  1425 + "dev": true,
  1426 + "funding": [
  1427 + {
  1428 + "type": "opencollective",
  1429 + "url": "https://opencollective.com/browserslist"
  1430 + },
  1431 + {
  1432 + "type": "tidelift",
  1433 + "url": "https://tidelift.com/funding/github/npm/browserslist"
  1434 + },
  1435 + {
  1436 + "type": "github",
  1437 + "url": "https://github.com/sponsors/ai"
  1438 + }
  1439 + ],
  1440 + "license": "MIT",
  1441 + "dependencies": {
  1442 + "baseline-browser-mapping": "^2.10.12",
  1443 + "caniuse-lite": "^1.0.30001782",
  1444 + "electron-to-chromium": "^1.5.328",
  1445 + "node-releases": "^2.0.36",
  1446 + "update-browserslist-db": "^1.2.3"
  1447 + },
  1448 + "bin": {
  1449 + "browserslist": "cli.js"
  1450 + },
  1451 + "engines": {
  1452 + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
  1453 + }
  1454 + },
  1455 + "node_modules/camelcase-css": {
  1456 + "version": "2.0.1",
  1457 + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
  1458 + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
  1459 + "dev": true,
  1460 + "license": "MIT",
  1461 + "engines": {
  1462 + "node": ">= 6"
  1463 + }
  1464 + },
  1465 + "node_modules/caniuse-lite": {
  1466 + "version": "1.0.30001787",
  1467 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz",
  1468 + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==",
  1469 + "dev": true,
  1470 + "funding": [
  1471 + {
  1472 + "type": "opencollective",
  1473 + "url": "https://opencollective.com/browserslist"
  1474 + },
  1475 + {
  1476 + "type": "tidelift",
  1477 + "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
  1478 + },
  1479 + {
  1480 + "type": "github",
  1481 + "url": "https://github.com/sponsors/ai"
  1482 + }
  1483 + ],
  1484 + "license": "CC-BY-4.0"
  1485 + },
  1486 + "node_modules/chokidar": {
  1487 + "version": "3.6.0",
  1488 + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
  1489 + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
  1490 + "dev": true,
  1491 + "license": "MIT",
  1492 + "dependencies": {
  1493 + "anymatch": "~3.1.2",
  1494 + "braces": "~3.0.2",
  1495 + "glob-parent": "~5.1.2",
  1496 + "is-binary-path": "~2.1.0",
  1497 + "is-glob": "~4.0.1",
  1498 + "normalize-path": "~3.0.0",
  1499 + "readdirp": "~3.6.0"
  1500 + },
  1501 + "engines": {
  1502 + "node": ">= 8.10.0"
  1503 + },
  1504 + "funding": {
  1505 + "url": "https://paulmillr.com/funding/"
  1506 + },
  1507 + "optionalDependencies": {
  1508 + "fsevents": "~2.3.2"
  1509 + }
  1510 + },
  1511 + "node_modules/chokidar/node_modules/glob-parent": {
  1512 + "version": "5.1.2",
  1513 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
  1514 + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
  1515 + "dev": true,
  1516 + "license": "ISC",
  1517 + "dependencies": {
  1518 + "is-glob": "^4.0.1"
  1519 + },
  1520 + "engines": {
  1521 + "node": ">= 6"
  1522 + }
  1523 + },
  1524 + "node_modules/commander": {
  1525 + "version": "4.1.1",
  1526 + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
  1527 + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
  1528 + "dev": true,
  1529 + "license": "MIT",
  1530 + "engines": {
  1531 + "node": ">= 6"
  1532 + }
  1533 + },
  1534 + "node_modules/convert-source-map": {
  1535 + "version": "2.0.0",
  1536 + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
  1537 + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
  1538 + "dev": true,
  1539 + "license": "MIT"
  1540 + },
  1541 + "node_modules/cssesc": {
  1542 + "version": "3.0.0",
  1543 + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
  1544 + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
  1545 + "dev": true,
  1546 + "license": "MIT",
  1547 + "bin": {
  1548 + "cssesc": "bin/cssesc"
  1549 + },
  1550 + "engines": {
  1551 + "node": ">=4"
  1552 + }
  1553 + },
  1554 + "node_modules/csstype": {
  1555 + "version": "3.2.3",
  1556 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
  1557 + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
  1558 + "dev": true,
  1559 + "license": "MIT"
  1560 + },
  1561 + "node_modules/debug": {
  1562 + "version": "4.4.3",
  1563 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
  1564 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
  1565 + "dev": true,
  1566 + "license": "MIT",
  1567 + "dependencies": {
  1568 + "ms": "^2.1.3"
  1569 + },
  1570 + "engines": {
  1571 + "node": ">=6.0"
  1572 + },
  1573 + "peerDependenciesMeta": {
  1574 + "supports-color": {
  1575 + "optional": true
  1576 + }
  1577 + }
  1578 + },
  1579 + "node_modules/didyoumean": {
  1580 + "version": "1.2.2",
  1581 + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
  1582 + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
  1583 + "dev": true,
  1584 + "license": "Apache-2.0"
  1585 + },
  1586 + "node_modules/dlv": {
  1587 + "version": "1.1.3",
  1588 + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
  1589 + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
  1590 + "dev": true,
  1591 + "license": "MIT"
  1592 + },
  1593 + "node_modules/electron-to-chromium": {
  1594 + "version": "1.5.334",
  1595 + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz",
  1596 + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==",
  1597 + "dev": true,
  1598 + "license": "ISC"
  1599 + },
  1600 + "node_modules/esbuild": {
  1601 + "version": "0.21.5",
  1602 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
  1603 + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
  1604 + "dev": true,
  1605 + "hasInstallScript": true,
  1606 + "license": "MIT",
  1607 + "bin": {
  1608 + "esbuild": "bin/esbuild"
  1609 + },
  1610 + "engines": {
  1611 + "node": ">=12"
  1612 + },
  1613 + "optionalDependencies": {
  1614 + "@esbuild/aix-ppc64": "0.21.5",
  1615 + "@esbuild/android-arm": "0.21.5",
  1616 + "@esbuild/android-arm64": "0.21.5",
  1617 + "@esbuild/android-x64": "0.21.5",
  1618 + "@esbuild/darwin-arm64": "0.21.5",
  1619 + "@esbuild/darwin-x64": "0.21.5",
  1620 + "@esbuild/freebsd-arm64": "0.21.5",
  1621 + "@esbuild/freebsd-x64": "0.21.5",
  1622 + "@esbuild/linux-arm": "0.21.5",
  1623 + "@esbuild/linux-arm64": "0.21.5",
  1624 + "@esbuild/linux-ia32": "0.21.5",
  1625 + "@esbuild/linux-loong64": "0.21.5",
  1626 + "@esbuild/linux-mips64el": "0.21.5",
  1627 + "@esbuild/linux-ppc64": "0.21.5",
  1628 + "@esbuild/linux-riscv64": "0.21.5",
  1629 + "@esbuild/linux-s390x": "0.21.5",
  1630 + "@esbuild/linux-x64": "0.21.5",
  1631 + "@esbuild/netbsd-x64": "0.21.5",
  1632 + "@esbuild/openbsd-x64": "0.21.5",
  1633 + "@esbuild/sunos-x64": "0.21.5",
  1634 + "@esbuild/win32-arm64": "0.21.5",
  1635 + "@esbuild/win32-ia32": "0.21.5",
  1636 + "@esbuild/win32-x64": "0.21.5"
  1637 + }
  1638 + },
  1639 + "node_modules/escalade": {
  1640 + "version": "3.2.0",
  1641 + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
  1642 + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
  1643 + "dev": true,
  1644 + "license": "MIT",
  1645 + "engines": {
  1646 + "node": ">=6"
  1647 + }
  1648 + },
  1649 + "node_modules/fast-glob": {
  1650 + "version": "3.3.3",
  1651 + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
  1652 + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
  1653 + "dev": true,
  1654 + "license": "MIT",
  1655 + "dependencies": {
  1656 + "@nodelib/fs.stat": "^2.0.2",
  1657 + "@nodelib/fs.walk": "^1.2.3",
  1658 + "glob-parent": "^5.1.2",
  1659 + "merge2": "^1.3.0",
  1660 + "micromatch": "^4.0.8"
  1661 + },
  1662 + "engines": {
  1663 + "node": ">=8.6.0"
  1664 + }
  1665 + },
  1666 + "node_modules/fast-glob/node_modules/glob-parent": {
  1667 + "version": "5.1.2",
  1668 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
  1669 + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
  1670 + "dev": true,
  1671 + "license": "ISC",
  1672 + "dependencies": {
  1673 + "is-glob": "^4.0.1"
  1674 + },
  1675 + "engines": {
  1676 + "node": ">= 6"
  1677 + }
  1678 + },
  1679 + "node_modules/fastq": {
  1680 + "version": "1.20.1",
  1681 + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
  1682 + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
  1683 + "dev": true,
  1684 + "license": "ISC",
  1685 + "dependencies": {
  1686 + "reusify": "^1.0.4"
  1687 + }
  1688 + },
  1689 + "node_modules/fill-range": {
  1690 + "version": "7.1.1",
  1691 + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
  1692 + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
  1693 + "dev": true,
  1694 + "license": "MIT",
  1695 + "dependencies": {
  1696 + "to-regex-range": "^5.0.1"
  1697 + },
  1698 + "engines": {
  1699 + "node": ">=8"
  1700 + }
  1701 + },
  1702 + "node_modules/fraction.js": {
  1703 + "version": "5.3.4",
  1704 + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
  1705 + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
  1706 + "dev": true,
  1707 + "license": "MIT",
  1708 + "engines": {
  1709 + "node": "*"
  1710 + },
  1711 + "funding": {
  1712 + "type": "github",
  1713 + "url": "https://github.com/sponsors/rawify"
  1714 + }
  1715 + },
  1716 + "node_modules/fsevents": {
  1717 + "version": "2.3.3",
  1718 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
  1719 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
  1720 + "dev": true,
  1721 + "hasInstallScript": true,
  1722 + "license": "MIT",
  1723 + "optional": true,
  1724 + "os": [
  1725 + "darwin"
  1726 + ],
  1727 + "engines": {
  1728 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
  1729 + }
  1730 + },
  1731 + "node_modules/function-bind": {
  1732 + "version": "1.1.2",
  1733 + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
  1734 + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
  1735 + "dev": true,
  1736 + "license": "MIT",
  1737 + "funding": {
  1738 + "url": "https://github.com/sponsors/ljharb"
  1739 + }
  1740 + },
  1741 + "node_modules/gensync": {
  1742 + "version": "1.0.0-beta.2",
  1743 + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
  1744 + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
  1745 + "dev": true,
  1746 + "license": "MIT",
  1747 + "engines": {
  1748 + "node": ">=6.9.0"
  1749 + }
  1750 + },
  1751 + "node_modules/glob-parent": {
  1752 + "version": "6.0.2",
  1753 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
  1754 + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
  1755 + "dev": true,
  1756 + "license": "ISC",
  1757 + "dependencies": {
  1758 + "is-glob": "^4.0.3"
  1759 + },
  1760 + "engines": {
  1761 + "node": ">=10.13.0"
  1762 + }
  1763 + },
  1764 + "node_modules/hasown": {
  1765 + "version": "2.0.2",
  1766 + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
  1767 + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
  1768 + "dev": true,
  1769 + "license": "MIT",
  1770 + "dependencies": {
  1771 + "function-bind": "^1.1.2"
  1772 + },
  1773 + "engines": {
  1774 + "node": ">= 0.4"
  1775 + }
  1776 + },
  1777 + "node_modules/is-binary-path": {
  1778 + "version": "2.1.0",
  1779 + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
  1780 + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
  1781 + "dev": true,
  1782 + "license": "MIT",
  1783 + "dependencies": {
  1784 + "binary-extensions": "^2.0.0"
  1785 + },
  1786 + "engines": {
  1787 + "node": ">=8"
  1788 + }
  1789 + },
  1790 + "node_modules/is-core-module": {
  1791 + "version": "2.16.1",
  1792 + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
  1793 + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
  1794 + "dev": true,
  1795 + "license": "MIT",
  1796 + "dependencies": {
  1797 + "hasown": "^2.0.2"
  1798 + },
  1799 + "engines": {
  1800 + "node": ">= 0.4"
  1801 + },
  1802 + "funding": {
  1803 + "url": "https://github.com/sponsors/ljharb"
  1804 + }
  1805 + },
  1806 + "node_modules/is-extglob": {
  1807 + "version": "2.1.1",
  1808 + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
  1809 + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
  1810 + "dev": true,
  1811 + "license": "MIT",
  1812 + "engines": {
  1813 + "node": ">=0.10.0"
  1814 + }
  1815 + },
  1816 + "node_modules/is-glob": {
  1817 + "version": "4.0.3",
  1818 + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
  1819 + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
  1820 + "dev": true,
  1821 + "license": "MIT",
  1822 + "dependencies": {
  1823 + "is-extglob": "^2.1.1"
  1824 + },
  1825 + "engines": {
  1826 + "node": ">=0.10.0"
  1827 + }
  1828 + },
  1829 + "node_modules/is-number": {
  1830 + "version": "7.0.0",
  1831 + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
  1832 + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
  1833 + "dev": true,
  1834 + "license": "MIT",
  1835 + "engines": {
  1836 + "node": ">=0.12.0"
  1837 + }
  1838 + },
  1839 + "node_modules/jiti": {
  1840 + "version": "1.21.7",
  1841 + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
  1842 + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
  1843 + "dev": true,
  1844 + "license": "MIT",
  1845 + "bin": {
  1846 + "jiti": "bin/jiti.js"
  1847 + }
  1848 + },
  1849 + "node_modules/js-tokens": {
  1850 + "version": "4.0.0",
  1851 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
  1852 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
  1853 + "license": "MIT"
  1854 + },
  1855 + "node_modules/jsesc": {
  1856 + "version": "3.1.0",
  1857 + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
  1858 + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
  1859 + "dev": true,
  1860 + "license": "MIT",
  1861 + "bin": {
  1862 + "jsesc": "bin/jsesc"
  1863 + },
  1864 + "engines": {
  1865 + "node": ">=6"
  1866 + }
  1867 + },
  1868 + "node_modules/json5": {
  1869 + "version": "2.2.3",
  1870 + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
  1871 + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
  1872 + "dev": true,
  1873 + "license": "MIT",
  1874 + "bin": {
  1875 + "json5": "lib/cli.js"
  1876 + },
  1877 + "engines": {
  1878 + "node": ">=6"
  1879 + }
  1880 + },
  1881 + "node_modules/lilconfig": {
  1882 + "version": "3.1.3",
  1883 + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
  1884 + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
  1885 + "dev": true,
  1886 + "license": "MIT",
  1887 + "engines": {
  1888 + "node": ">=14"
  1889 + },
  1890 + "funding": {
  1891 + "url": "https://github.com/sponsors/antonk52"
  1892 + }
  1893 + },
  1894 + "node_modules/lines-and-columns": {
  1895 + "version": "1.2.4",
  1896 + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
  1897 + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
  1898 + "dev": true,
  1899 + "license": "MIT"
  1900 + },
  1901 + "node_modules/loose-envify": {
  1902 + "version": "1.4.0",
  1903 + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
  1904 + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
  1905 + "license": "MIT",
  1906 + "dependencies": {
  1907 + "js-tokens": "^3.0.0 || ^4.0.0"
  1908 + },
  1909 + "bin": {
  1910 + "loose-envify": "cli.js"
  1911 + }
  1912 + },
  1913 + "node_modules/lru-cache": {
  1914 + "version": "5.1.1",
  1915 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
  1916 + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
  1917 + "dev": true,
  1918 + "license": "ISC",
  1919 + "dependencies": {
  1920 + "yallist": "^3.0.2"
  1921 + }
  1922 + },
  1923 + "node_modules/merge2": {
  1924 + "version": "1.4.1",
  1925 + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
  1926 + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
  1927 + "dev": true,
  1928 + "license": "MIT",
  1929 + "engines": {
  1930 + "node": ">= 8"
  1931 + }
  1932 + },
  1933 + "node_modules/micromatch": {
  1934 + "version": "4.0.8",
  1935 + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
  1936 + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
  1937 + "dev": true,
  1938 + "license": "MIT",
  1939 + "dependencies": {
  1940 + "braces": "^3.0.3",
  1941 + "picomatch": "^2.3.1"
  1942 + },
  1943 + "engines": {
  1944 + "node": ">=8.6"
  1945 + }
  1946 + },
  1947 + "node_modules/ms": {
  1948 + "version": "2.1.3",
  1949 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
  1950 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
  1951 + "dev": true,
  1952 + "license": "MIT"
  1953 + },
  1954 + "node_modules/mz": {
  1955 + "version": "2.7.0",
  1956 + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
  1957 + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
  1958 + "dev": true,
  1959 + "license": "MIT",
  1960 + "dependencies": {
  1961 + "any-promise": "^1.0.0",
  1962 + "object-assign": "^4.0.1",
  1963 + "thenify-all": "^1.0.0"
  1964 + }
  1965 + },
  1966 + "node_modules/nanoid": {
  1967 + "version": "3.3.11",
  1968 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
  1969 + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
  1970 + "dev": true,
  1971 + "funding": [
  1972 + {
  1973 + "type": "github",
  1974 + "url": "https://github.com/sponsors/ai"
  1975 + }
  1976 + ],
  1977 + "license": "MIT",
  1978 + "bin": {
  1979 + "nanoid": "bin/nanoid.cjs"
  1980 + },
  1981 + "engines": {
  1982 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
  1983 + }
  1984 + },
  1985 + "node_modules/node-releases": {
  1986 + "version": "2.0.37",
  1987 + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
  1988 + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
  1989 + "dev": true,
  1990 + "license": "MIT"
  1991 + },
  1992 + "node_modules/normalize-path": {
  1993 + "version": "3.0.0",
  1994 + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
  1995 + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
  1996 + "dev": true,
  1997 + "license": "MIT",
  1998 + "engines": {
  1999 + "node": ">=0.10.0"
  2000 + }
  2001 + },
  2002 + "node_modules/object-assign": {
  2003 + "version": "4.1.1",
  2004 + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
  2005 + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
  2006 + "dev": true,
  2007 + "license": "MIT",
  2008 + "engines": {
  2009 + "node": ">=0.10.0"
  2010 + }
  2011 + },
  2012 + "node_modules/object-hash": {
  2013 + "version": "3.0.0",
  2014 + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
  2015 + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
  2016 + "dev": true,
  2017 + "license": "MIT",
  2018 + "engines": {
  2019 + "node": ">= 6"
  2020 + }
  2021 + },
  2022 + "node_modules/path-parse": {
  2023 + "version": "1.0.7",
  2024 + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
  2025 + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
  2026 + "dev": true,
  2027 + "license": "MIT"
  2028 + },
  2029 + "node_modules/picocolors": {
  2030 + "version": "1.1.1",
  2031 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
  2032 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
  2033 + "dev": true,
  2034 + "license": "ISC"
  2035 + },
  2036 + "node_modules/picomatch": {
  2037 + "version": "2.3.2",
  2038 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
  2039 + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
  2040 + "dev": true,
  2041 + "license": "MIT",
  2042 + "engines": {
  2043 + "node": ">=8.6"
  2044 + },
  2045 + "funding": {
  2046 + "url": "https://github.com/sponsors/jonschlinkert"
  2047 + }
  2048 + },
  2049 + "node_modules/pify": {
  2050 + "version": "2.3.0",
  2051 + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
  2052 + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
  2053 + "dev": true,
  2054 + "license": "MIT",
  2055 + "engines": {
  2056 + "node": ">=0.10.0"
  2057 + }
  2058 + },
  2059 + "node_modules/pirates": {
  2060 + "version": "4.0.7",
  2061 + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
  2062 + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
  2063 + "dev": true,
  2064 + "license": "MIT",
  2065 + "engines": {
  2066 + "node": ">= 6"
  2067 + }
  2068 + },
  2069 + "node_modules/postcss": {
  2070 + "version": "8.5.9",
  2071 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
  2072 + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
  2073 + "dev": true,
  2074 + "funding": [
  2075 + {
  2076 + "type": "opencollective",
  2077 + "url": "https://opencollective.com/postcss/"
  2078 + },
  2079 + {
  2080 + "type": "tidelift",
  2081 + "url": "https://tidelift.com/funding/github/npm/postcss"
  2082 + },
  2083 + {
  2084 + "type": "github",
  2085 + "url": "https://github.com/sponsors/ai"
  2086 + }
  2087 + ],
  2088 + "license": "MIT",
  2089 + "dependencies": {
  2090 + "nanoid": "^3.3.11",
  2091 + "picocolors": "^1.1.1",
  2092 + "source-map-js": "^1.2.1"
  2093 + },
  2094 + "engines": {
  2095 + "node": "^10 || ^12 || >=14"
  2096 + }
  2097 + },
  2098 + "node_modules/postcss-import": {
  2099 + "version": "15.1.0",
  2100 + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
  2101 + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
  2102 + "dev": true,
  2103 + "license": "MIT",
  2104 + "dependencies": {
  2105 + "postcss-value-parser": "^4.0.0",
  2106 + "read-cache": "^1.0.0",
  2107 + "resolve": "^1.1.7"
  2108 + },
  2109 + "engines": {
  2110 + "node": ">=14.0.0"
  2111 + },
  2112 + "peerDependencies": {
  2113 + "postcss": "^8.0.0"
  2114 + }
  2115 + },
  2116 + "node_modules/postcss-js": {
  2117 + "version": "4.1.0",
  2118 + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
  2119 + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
  2120 + "dev": true,
  2121 + "funding": [
  2122 + {
  2123 + "type": "opencollective",
  2124 + "url": "https://opencollective.com/postcss/"
  2125 + },
  2126 + {
  2127 + "type": "github",
  2128 + "url": "https://github.com/sponsors/ai"
  2129 + }
  2130 + ],
  2131 + "license": "MIT",
  2132 + "dependencies": {
  2133 + "camelcase-css": "^2.0.1"
  2134 + },
  2135 + "engines": {
  2136 + "node": "^12 || ^14 || >= 16"
  2137 + },
  2138 + "peerDependencies": {
  2139 + "postcss": "^8.4.21"
  2140 + }
  2141 + },
  2142 + "node_modules/postcss-load-config": {
  2143 + "version": "6.0.1",
  2144 + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
  2145 + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
  2146 + "dev": true,
  2147 + "funding": [
  2148 + {
  2149 + "type": "opencollective",
  2150 + "url": "https://opencollective.com/postcss/"
  2151 + },
  2152 + {
  2153 + "type": "github",
  2154 + "url": "https://github.com/sponsors/ai"
  2155 + }
  2156 + ],
  2157 + "license": "MIT",
  2158 + "dependencies": {
  2159 + "lilconfig": "^3.1.1"
  2160 + },
  2161 + "engines": {
  2162 + "node": ">= 18"
  2163 + },
  2164 + "peerDependencies": {
  2165 + "jiti": ">=1.21.0",
  2166 + "postcss": ">=8.0.9",
  2167 + "tsx": "^4.8.1",
  2168 + "yaml": "^2.4.2"
  2169 + },
  2170 + "peerDependenciesMeta": {
  2171 + "jiti": {
  2172 + "optional": true
  2173 + },
  2174 + "postcss": {
  2175 + "optional": true
  2176 + },
  2177 + "tsx": {
  2178 + "optional": true
  2179 + },
  2180 + "yaml": {
  2181 + "optional": true
  2182 + }
  2183 + }
  2184 + },
  2185 + "node_modules/postcss-nested": {
  2186 + "version": "6.2.0",
  2187 + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
  2188 + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
  2189 + "dev": true,
  2190 + "funding": [
  2191 + {
  2192 + "type": "opencollective",
  2193 + "url": "https://opencollective.com/postcss/"
  2194 + },
  2195 + {
  2196 + "type": "github",
  2197 + "url": "https://github.com/sponsors/ai"
  2198 + }
  2199 + ],
  2200 + "license": "MIT",
  2201 + "dependencies": {
  2202 + "postcss-selector-parser": "^6.1.1"
  2203 + },
  2204 + "engines": {
  2205 + "node": ">=12.0"
  2206 + },
  2207 + "peerDependencies": {
  2208 + "postcss": "^8.2.14"
  2209 + }
  2210 + },
  2211 + "node_modules/postcss-selector-parser": {
  2212 + "version": "6.1.2",
  2213 + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
  2214 + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
  2215 + "dev": true,
  2216 + "license": "MIT",
  2217 + "dependencies": {
  2218 + "cssesc": "^3.0.0",
  2219 + "util-deprecate": "^1.0.2"
  2220 + },
  2221 + "engines": {
  2222 + "node": ">=4"
  2223 + }
  2224 + },
  2225 + "node_modules/postcss-value-parser": {
  2226 + "version": "4.2.0",
  2227 + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
  2228 + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
  2229 + "dev": true,
  2230 + "license": "MIT"
  2231 + },
  2232 + "node_modules/queue-microtask": {
  2233 + "version": "1.2.3",
  2234 + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
  2235 + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
  2236 + "dev": true,
  2237 + "funding": [
  2238 + {
  2239 + "type": "github",
  2240 + "url": "https://github.com/sponsors/feross"
  2241 + },
  2242 + {
  2243 + "type": "patreon",
  2244 + "url": "https://www.patreon.com/feross"
  2245 + },
  2246 + {
  2247 + "type": "consulting",
  2248 + "url": "https://feross.org/support"
  2249 + }
  2250 + ],
  2251 + "license": "MIT"
  2252 + },
  2253 + "node_modules/react": {
  2254 + "version": "18.3.1",
  2255 + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
  2256 + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
  2257 + "license": "MIT",
  2258 + "dependencies": {
  2259 + "loose-envify": "^1.1.0"
  2260 + },
  2261 + "engines": {
  2262 + "node": ">=0.10.0"
  2263 + }
  2264 + },
  2265 + "node_modules/react-dom": {
  2266 + "version": "18.3.1",
  2267 + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
  2268 + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
  2269 + "license": "MIT",
  2270 + "dependencies": {
  2271 + "loose-envify": "^1.1.0",
  2272 + "scheduler": "^0.23.2"
  2273 + },
  2274 + "peerDependencies": {
  2275 + "react": "^18.3.1"
  2276 + }
  2277 + },
  2278 + "node_modules/react-refresh": {
  2279 + "version": "0.17.0",
  2280 + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
  2281 + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
  2282 + "dev": true,
  2283 + "license": "MIT",
  2284 + "engines": {
  2285 + "node": ">=0.10.0"
  2286 + }
  2287 + },
  2288 + "node_modules/react-router": {
  2289 + "version": "6.30.3",
  2290 + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
  2291 + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
  2292 + "license": "MIT",
  2293 + "dependencies": {
  2294 + "@remix-run/router": "1.23.2"
  2295 + },
  2296 + "engines": {
  2297 + "node": ">=14.0.0"
  2298 + },
  2299 + "peerDependencies": {
  2300 + "react": ">=16.8"
  2301 + }
  2302 + },
  2303 + "node_modules/react-router-dom": {
  2304 + "version": "6.30.3",
  2305 + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
  2306 + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
  2307 + "license": "MIT",
  2308 + "dependencies": {
  2309 + "@remix-run/router": "1.23.2",
  2310 + "react-router": "6.30.3"
  2311 + },
  2312 + "engines": {
  2313 + "node": ">=14.0.0"
  2314 + },
  2315 + "peerDependencies": {
  2316 + "react": ">=16.8",
  2317 + "react-dom": ">=16.8"
  2318 + }
  2319 + },
  2320 + "node_modules/read-cache": {
  2321 + "version": "1.0.0",
  2322 + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
  2323 + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
  2324 + "dev": true,
  2325 + "license": "MIT",
  2326 + "dependencies": {
  2327 + "pify": "^2.3.0"
  2328 + }
  2329 + },
  2330 + "node_modules/readdirp": {
  2331 + "version": "3.6.0",
  2332 + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
  2333 + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
  2334 + "dev": true,
  2335 + "license": "MIT",
  2336 + "dependencies": {
  2337 + "picomatch": "^2.2.1"
  2338 + },
  2339 + "engines": {
  2340 + "node": ">=8.10.0"
  2341 + }
  2342 + },
  2343 + "node_modules/resolve": {
  2344 + "version": "1.22.11",
  2345 + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
  2346 + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
  2347 + "dev": true,
  2348 + "license": "MIT",
  2349 + "dependencies": {
  2350 + "is-core-module": "^2.16.1",
  2351 + "path-parse": "^1.0.7",
  2352 + "supports-preserve-symlinks-flag": "^1.0.0"
  2353 + },
  2354 + "bin": {
  2355 + "resolve": "bin/resolve"
  2356 + },
  2357 + "engines": {
  2358 + "node": ">= 0.4"
  2359 + },
  2360 + "funding": {
  2361 + "url": "https://github.com/sponsors/ljharb"
  2362 + }
  2363 + },
  2364 + "node_modules/reusify": {
  2365 + "version": "1.1.0",
  2366 + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
  2367 + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
  2368 + "dev": true,
  2369 + "license": "MIT",
  2370 + "engines": {
  2371 + "iojs": ">=1.0.0",
  2372 + "node": ">=0.10.0"
  2373 + }
  2374 + },
  2375 + "node_modules/rollup": {
  2376 + "version": "4.60.1",
  2377 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
  2378 + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
  2379 + "dev": true,
  2380 + "license": "MIT",
  2381 + "dependencies": {
  2382 + "@types/estree": "1.0.8"
  2383 + },
  2384 + "bin": {
  2385 + "rollup": "dist/bin/rollup"
  2386 + },
  2387 + "engines": {
  2388 + "node": ">=18.0.0",
  2389 + "npm": ">=8.0.0"
  2390 + },
  2391 + "optionalDependencies": {
  2392 + "@rollup/rollup-android-arm-eabi": "4.60.1",
  2393 + "@rollup/rollup-android-arm64": "4.60.1",
  2394 + "@rollup/rollup-darwin-arm64": "4.60.1",
  2395 + "@rollup/rollup-darwin-x64": "4.60.1",
  2396 + "@rollup/rollup-freebsd-arm64": "4.60.1",
  2397 + "@rollup/rollup-freebsd-x64": "4.60.1",
  2398 + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
  2399 + "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
  2400 + "@rollup/rollup-linux-arm64-gnu": "4.60.1",
  2401 + "@rollup/rollup-linux-arm64-musl": "4.60.1",
  2402 + "@rollup/rollup-linux-loong64-gnu": "4.60.1",
  2403 + "@rollup/rollup-linux-loong64-musl": "4.60.1",
  2404 + "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
  2405 + "@rollup/rollup-linux-ppc64-musl": "4.60.1",
  2406 + "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
  2407 + "@rollup/rollup-linux-riscv64-musl": "4.60.1",
  2408 + "@rollup/rollup-linux-s390x-gnu": "4.60.1",
  2409 + "@rollup/rollup-linux-x64-gnu": "4.60.1",
  2410 + "@rollup/rollup-linux-x64-musl": "4.60.1",
  2411 + "@rollup/rollup-openbsd-x64": "4.60.1",
  2412 + "@rollup/rollup-openharmony-arm64": "4.60.1",
  2413 + "@rollup/rollup-win32-arm64-msvc": "4.60.1",
  2414 + "@rollup/rollup-win32-ia32-msvc": "4.60.1",
  2415 + "@rollup/rollup-win32-x64-gnu": "4.60.1",
  2416 + "@rollup/rollup-win32-x64-msvc": "4.60.1",
  2417 + "fsevents": "~2.3.2"
  2418 + }
  2419 + },
  2420 + "node_modules/run-parallel": {
  2421 + "version": "1.2.0",
  2422 + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
  2423 + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
  2424 + "dev": true,
  2425 + "funding": [
  2426 + {
  2427 + "type": "github",
  2428 + "url": "https://github.com/sponsors/feross"
  2429 + },
  2430 + {
  2431 + "type": "patreon",
  2432 + "url": "https://www.patreon.com/feross"
  2433 + },
  2434 + {
  2435 + "type": "consulting",
  2436 + "url": "https://feross.org/support"
  2437 + }
  2438 + ],
  2439 + "license": "MIT",
  2440 + "dependencies": {
  2441 + "queue-microtask": "^1.2.2"
  2442 + }
  2443 + },
  2444 + "node_modules/scheduler": {
  2445 + "version": "0.23.2",
  2446 + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
  2447 + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
  2448 + "license": "MIT",
  2449 + "dependencies": {
  2450 + "loose-envify": "^1.1.0"
  2451 + }
  2452 + },
  2453 + "node_modules/semver": {
  2454 + "version": "6.3.1",
  2455 + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
  2456 + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
  2457 + "dev": true,
  2458 + "license": "ISC",
  2459 + "bin": {
  2460 + "semver": "bin/semver.js"
  2461 + }
  2462 + },
  2463 + "node_modules/source-map-js": {
  2464 + "version": "1.2.1",
  2465 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
  2466 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
  2467 + "dev": true,
  2468 + "license": "BSD-3-Clause",
  2469 + "engines": {
  2470 + "node": ">=0.10.0"
  2471 + }
  2472 + },
  2473 + "node_modules/sucrase": {
  2474 + "version": "3.35.1",
  2475 + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
  2476 + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
  2477 + "dev": true,
  2478 + "license": "MIT",
  2479 + "dependencies": {
  2480 + "@jridgewell/gen-mapping": "^0.3.2",
  2481 + "commander": "^4.0.0",
  2482 + "lines-and-columns": "^1.1.6",
  2483 + "mz": "^2.7.0",
  2484 + "pirates": "^4.0.1",
  2485 + "tinyglobby": "^0.2.11",
  2486 + "ts-interface-checker": "^0.1.9"
  2487 + },
  2488 + "bin": {
  2489 + "sucrase": "bin/sucrase",
  2490 + "sucrase-node": "bin/sucrase-node"
  2491 + },
  2492 + "engines": {
  2493 + "node": ">=16 || 14 >=14.17"
  2494 + }
  2495 + },
  2496 + "node_modules/supports-preserve-symlinks-flag": {
  2497 + "version": "1.0.0",
  2498 + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
  2499 + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
  2500 + "dev": true,
  2501 + "license": "MIT",
  2502 + "engines": {
  2503 + "node": ">= 0.4"
  2504 + },
  2505 + "funding": {
  2506 + "url": "https://github.com/sponsors/ljharb"
  2507 + }
  2508 + },
  2509 + "node_modules/tailwindcss": {
  2510 + "version": "3.4.19",
  2511 + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
  2512 + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
  2513 + "dev": true,
  2514 + "license": "MIT",
  2515 + "dependencies": {
  2516 + "@alloc/quick-lru": "^5.2.0",
  2517 + "arg": "^5.0.2",
  2518 + "chokidar": "^3.6.0",
  2519 + "didyoumean": "^1.2.2",
  2520 + "dlv": "^1.1.3",
  2521 + "fast-glob": "^3.3.2",
  2522 + "glob-parent": "^6.0.2",
  2523 + "is-glob": "^4.0.3",
  2524 + "jiti": "^1.21.7",
  2525 + "lilconfig": "^3.1.3",
  2526 + "micromatch": "^4.0.8",
  2527 + "normalize-path": "^3.0.0",
  2528 + "object-hash": "^3.0.0",
  2529 + "picocolors": "^1.1.1",
  2530 + "postcss": "^8.4.47",
  2531 + "postcss-import": "^15.1.0",
  2532 + "postcss-js": "^4.0.1",
  2533 + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
  2534 + "postcss-nested": "^6.2.0",
  2535 + "postcss-selector-parser": "^6.1.2",
  2536 + "resolve": "^1.22.8",
  2537 + "sucrase": "^3.35.0"
  2538 + },
  2539 + "bin": {
  2540 + "tailwind": "lib/cli.js",
  2541 + "tailwindcss": "lib/cli.js"
  2542 + },
  2543 + "engines": {
  2544 + "node": ">=14.0.0"
  2545 + }
  2546 + },
  2547 + "node_modules/thenify": {
  2548 + "version": "3.3.1",
  2549 + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
  2550 + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
  2551 + "dev": true,
  2552 + "license": "MIT",
  2553 + "dependencies": {
  2554 + "any-promise": "^1.0.0"
  2555 + }
  2556 + },
  2557 + "node_modules/thenify-all": {
  2558 + "version": "1.6.0",
  2559 + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
  2560 + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
  2561 + "dev": true,
  2562 + "license": "MIT",
  2563 + "dependencies": {
  2564 + "thenify": ">= 3.1.0 < 4"
  2565 + },
  2566 + "engines": {
  2567 + "node": ">=0.8"
  2568 + }
  2569 + },
  2570 + "node_modules/tinyglobby": {
  2571 + "version": "0.2.16",
  2572 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
  2573 + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
  2574 + "dev": true,
  2575 + "license": "MIT",
  2576 + "dependencies": {
  2577 + "fdir": "^6.5.0",
  2578 + "picomatch": "^4.0.4"
  2579 + },
  2580 + "engines": {
  2581 + "node": ">=12.0.0"
  2582 + },
  2583 + "funding": {
  2584 + "url": "https://github.com/sponsors/SuperchupuDev"
  2585 + }
  2586 + },
  2587 + "node_modules/tinyglobby/node_modules/fdir": {
  2588 + "version": "6.5.0",
  2589 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
  2590 + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
  2591 + "dev": true,
  2592 + "license": "MIT",
  2593 + "engines": {
  2594 + "node": ">=12.0.0"
  2595 + },
  2596 + "peerDependencies": {
  2597 + "picomatch": "^3 || ^4"
  2598 + },
  2599 + "peerDependenciesMeta": {
  2600 + "picomatch": {
  2601 + "optional": true
  2602 + }
  2603 + }
  2604 + },
  2605 + "node_modules/tinyglobby/node_modules/picomatch": {
  2606 + "version": "4.0.4",
  2607 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
  2608 + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
  2609 + "dev": true,
  2610 + "license": "MIT",
  2611 + "engines": {
  2612 + "node": ">=12"
  2613 + },
  2614 + "funding": {
  2615 + "url": "https://github.com/sponsors/jonschlinkert"
  2616 + }
  2617 + },
  2618 + "node_modules/to-regex-range": {
  2619 + "version": "5.0.1",
  2620 + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
  2621 + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
  2622 + "dev": true,
  2623 + "license": "MIT",
  2624 + "dependencies": {
  2625 + "is-number": "^7.0.0"
  2626 + },
  2627 + "engines": {
  2628 + "node": ">=8.0"
  2629 + }
  2630 + },
  2631 + "node_modules/ts-interface-checker": {
  2632 + "version": "0.1.13",
  2633 + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
  2634 + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
  2635 + "dev": true,
  2636 + "license": "Apache-2.0"
  2637 + },
  2638 + "node_modules/typescript": {
  2639 + "version": "5.9.3",
  2640 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
  2641 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
  2642 + "dev": true,
  2643 + "license": "Apache-2.0",
  2644 + "bin": {
  2645 + "tsc": "bin/tsc",
  2646 + "tsserver": "bin/tsserver"
  2647 + },
  2648 + "engines": {
  2649 + "node": ">=14.17"
  2650 + }
  2651 + },
  2652 + "node_modules/undici-types": {
  2653 + "version": "7.18.2",
  2654 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
  2655 + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
  2656 + "dev": true,
  2657 + "license": "MIT"
  2658 + },
  2659 + "node_modules/update-browserslist-db": {
  2660 + "version": "1.2.3",
  2661 + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
  2662 + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
  2663 + "dev": true,
  2664 + "funding": [
  2665 + {
  2666 + "type": "opencollective",
  2667 + "url": "https://opencollective.com/browserslist"
  2668 + },
  2669 + {
  2670 + "type": "tidelift",
  2671 + "url": "https://tidelift.com/funding/github/npm/browserslist"
  2672 + },
  2673 + {
  2674 + "type": "github",
  2675 + "url": "https://github.com/sponsors/ai"
  2676 + }
  2677 + ],
  2678 + "license": "MIT",
  2679 + "dependencies": {
  2680 + "escalade": "^3.2.0",
  2681 + "picocolors": "^1.1.1"
  2682 + },
  2683 + "bin": {
  2684 + "update-browserslist-db": "cli.js"
  2685 + },
  2686 + "peerDependencies": {
  2687 + "browserslist": ">= 4.21.0"
  2688 + }
  2689 + },
  2690 + "node_modules/util-deprecate": {
  2691 + "version": "1.0.2",
  2692 + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
  2693 + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
  2694 + "dev": true,
  2695 + "license": "MIT"
  2696 + },
  2697 + "node_modules/vite": {
  2698 + "version": "5.4.21",
  2699 + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
  2700 + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
  2701 + "dev": true,
  2702 + "license": "MIT",
  2703 + "dependencies": {
  2704 + "esbuild": "^0.21.3",
  2705 + "postcss": "^8.4.43",
  2706 + "rollup": "^4.20.0"
  2707 + },
  2708 + "bin": {
  2709 + "vite": "bin/vite.js"
  2710 + },
  2711 + "engines": {
  2712 + "node": "^18.0.0 || >=20.0.0"
  2713 + },
  2714 + "funding": {
  2715 + "url": "https://github.com/vitejs/vite?sponsor=1"
  2716 + },
  2717 + "optionalDependencies": {
  2718 + "fsevents": "~2.3.3"
  2719 + },
  2720 + "peerDependencies": {
  2721 + "@types/node": "^18.0.0 || >=20.0.0",
  2722 + "less": "*",
  2723 + "lightningcss": "^1.21.0",
  2724 + "sass": "*",
  2725 + "sass-embedded": "*",
  2726 + "stylus": "*",
  2727 + "sugarss": "*",
  2728 + "terser": "^5.4.0"
  2729 + },
  2730 + "peerDependenciesMeta": {
  2731 + "@types/node": {
  2732 + "optional": true
  2733 + },
  2734 + "less": {
  2735 + "optional": true
  2736 + },
  2737 + "lightningcss": {
  2738 + "optional": true
  2739 + },
  2740 + "sass": {
  2741 + "optional": true
  2742 + },
  2743 + "sass-embedded": {
  2744 + "optional": true
  2745 + },
  2746 + "stylus": {
  2747 + "optional": true
  2748 + },
  2749 + "sugarss": {
  2750 + "optional": true
  2751 + },
  2752 + "terser": {
  2753 + "optional": true
  2754 + }
  2755 + }
  2756 + },
  2757 + "node_modules/yallist": {
  2758 + "version": "3.1.1",
  2759 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
  2760 + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
  2761 + "dev": true,
  2762 + "license": "ISC"
  2763 + }
  2764 + }
  2765 +}
... ...
web/package.json 0 → 100644
  1 +{
  2 + "name": "vibe-erp-web",
  3 + "private": true,
  4 + "version": "0.0.0",
  5 + "type": "module",
  6 + "scripts": {
  7 + "dev": "vite",
  8 + "build": "tsc -b && vite build",
  9 + "preview": "vite preview"
  10 + },
  11 + "dependencies": {
  12 + "react": "^18.3.1",
  13 + "react-dom": "^18.3.1",
  14 + "react-router-dom": "^6.28.0"
  15 + },
  16 + "devDependencies": {
  17 + "@types/node": "^25.5.2",
  18 + "@types/react": "^18.3.12",
  19 + "@types/react-dom": "^18.3.1",
  20 + "@vitejs/plugin-react": "^4.3.3",
  21 + "autoprefixer": "^10.4.20",
  22 + "postcss": "^8.4.49",
  23 + "tailwindcss": "^3.4.15",
  24 + "typescript": "^5.6.3",
  25 + "vite": "^5.4.11"
  26 + }
  27 +}
... ...
web/postcss.config.js 0 → 100644
  1 +export default {
  2 + plugins: {
  3 + tailwindcss: {},
  4 + autoprefixer: {},
  5 + },
  6 +}
... ...
web/src/App.tsx 0 → 100644
  1 +// vibe_erp web SPA route table.
  2 +//
  3 +// One <Routes> for the whole SPA: /login is open, everything else
  4 +// nests under <ProtectedRoute><AppLayout/>. The layout component
  5 +// renders <Outlet/> for the active child route.
  6 +//
  7 +// **Why a flat route table.** v1 has 12 screens; nested routing
  8 +// would just be ceremony. Future chunks can introduce nested
  9 +// routes when, e.g., the Settings area gets sub-tabs.
  10 +
  11 +import { Navigate, Route, Routes } from 'react-router-dom'
  12 +import { AppLayout } from '@/layout/AppLayout'
  13 +import { ProtectedRoute } from '@/components/ProtectedRoute'
  14 +import { LoginPage } from '@/pages/LoginPage'
  15 +import { DashboardPage } from '@/pages/DashboardPage'
  16 +import { ItemsPage } from '@/pages/ItemsPage'
  17 +import { UomsPage } from '@/pages/UomsPage'
  18 +import { PartnersPage } from '@/pages/PartnersPage'
  19 +import { LocationsPage } from '@/pages/LocationsPage'
  20 +import { BalancesPage } from '@/pages/BalancesPage'
  21 +import { MovementsPage } from '@/pages/MovementsPage'
  22 +import { SalesOrdersPage } from '@/pages/SalesOrdersPage'
  23 +import { SalesOrderDetailPage } from '@/pages/SalesOrderDetailPage'
  24 +import { PurchaseOrdersPage } from '@/pages/PurchaseOrdersPage'
  25 +import { PurchaseOrderDetailPage } from '@/pages/PurchaseOrderDetailPage'
  26 +import { WorkOrdersPage } from '@/pages/WorkOrdersPage'
  27 +import { WorkOrderDetailPage } from '@/pages/WorkOrderDetailPage'
  28 +import { ShopFloorPage } from '@/pages/ShopFloorPage'
  29 +import { JournalEntriesPage } from '@/pages/JournalEntriesPage'
  30 +
  31 +export default function App() {
  32 + return (
  33 + <Routes>
  34 + <Route path="/login" element={<LoginPage />} />
  35 + <Route
  36 + path="/"
  37 + element={
  38 + <ProtectedRoute>
  39 + <AppLayout />
  40 + </ProtectedRoute>
  41 + }
  42 + >
  43 + <Route index element={<DashboardPage />} />
  44 + <Route path="items" element={<ItemsPage />} />
  45 + <Route path="uoms" element={<UomsPage />} />
  46 + <Route path="partners" element={<PartnersPage />} />
  47 + <Route path="locations" element={<LocationsPage />} />
  48 + <Route path="balances" element={<BalancesPage />} />
  49 + <Route path="movements" element={<MovementsPage />} />
  50 + <Route path="sales-orders" element={<SalesOrdersPage />} />
  51 + <Route path="sales-orders/:id" element={<SalesOrderDetailPage />} />
  52 + <Route path="purchase-orders" element={<PurchaseOrdersPage />} />
  53 + <Route path="purchase-orders/:id" element={<PurchaseOrderDetailPage />} />
  54 + <Route path="work-orders" element={<WorkOrdersPage />} />
  55 + <Route path="work-orders/:id" element={<WorkOrderDetailPage />} />
  56 + <Route path="shop-floor" element={<ShopFloorPage />} />
  57 + <Route path="journal-entries" element={<JournalEntriesPage />} />
  58 + </Route>
  59 + <Route path="*" element={<Navigate to="/" replace />} />
  60 + </Routes>
  61 + )
  62 +}
... ...
web/src/api/client.ts 0 → 100644
  1 +// vibe_erp typed REST client.
  2 +//
  3 +// **Why a thin wrapper instead of a generated client.** The framework
  4 +// already serves a real OpenAPI spec at /v3/api-docs and could feed
  5 +// openapi-typescript or openapi-generator-cli — but every code
  6 +// generator pulls a chain of npm/Java toolchain steps into the build
  7 +// and generates ~thousands of lines of churn for every backend tweak.
  8 +// A hand-written client over `fetch` keeps the SPA bundle small,
  9 +// auditable, and dependency-free, and v1 of the SPA only needs the
  10 +// few dozen calls listed below. Codegen can return when (a) the
  11 +// API surface is too large to maintain by hand or (b) external
  12 +// integrators want a typed client they can reuse — neither is true
  13 +// in v1.0.
  14 +//
  15 +// **Auth.** Every call goes through `apiFetch`, which reads the
  16 +// access token from localStorage and adds an `Authorization: Bearer
  17 +// <token>` header. A 401 response triggers a hard logout (clears
  18 +// the token + redirects to /login via the auth context) — not a
  19 +// silent refresh, because v1 keeps the refresh story simple.
  20 +// Refreshing on 401 is a v1.x improvement.
  21 +//
  22 +// **Errors.** A non-2xx response throws an `ApiError` carrying the
  23 +// status, message, and the parsed JSON body if any. Pages catch
  24 +// this and render the message inline. Network failures throw the
  25 +// underlying TypeError, also caught by the page boundary.
  26 +
  27 +import type {
  28 + Item,
  29 + JournalEntry,
  30 + Location,
  31 + MetaInfo,
  32 + Partner,
  33 + PurchaseOrder,
  34 + SalesOrder,
  35 + ShopFloorEntry,
  36 + StockBalance,
  37 + StockMovement,
  38 + TokenPair,
  39 + Uom,
  40 + WorkOrder,
  41 +} from '@/types/api'
  42 +
  43 +const TOKEN_KEY = 'vibeerp.accessToken'
  44 +
  45 +export function getAccessToken(): string | null {
  46 + return localStorage.getItem(TOKEN_KEY)
  47 +}
  48 +
  49 +export function setAccessToken(token: string | null): void {
  50 + if (token === null) {
  51 + localStorage.removeItem(TOKEN_KEY)
  52 + } else {
  53 + localStorage.setItem(TOKEN_KEY, token)
  54 + }
  55 +}
  56 +
  57 +export class ApiError extends Error {
  58 + constructor(
  59 + message: string,
  60 + public readonly status: number,
  61 + public readonly body: unknown,
  62 + ) {
  63 + super(message)
  64 + this.name = 'ApiError'
  65 + }
  66 +}
  67 +
  68 +// One-time-set callback that the auth context registers so the
  69 +// client can trigger a logout on 401 without a circular import.
  70 +let onUnauthorized: (() => void) | null = null
  71 +export function registerUnauthorizedHandler(handler: () => void) {
  72 + onUnauthorized = handler
  73 +}
  74 +
  75 +async function apiFetch<T>(
  76 + path: string,
  77 + init: RequestInit = {},
  78 + expectJson = true,
  79 +): Promise<T> {
  80 + const headers = new Headers(init.headers)
  81 + headers.set('Accept', 'application/json')
  82 + if (init.body && !headers.has('Content-Type')) {
  83 + headers.set('Content-Type', 'application/json')
  84 + }
  85 + const token = getAccessToken()
  86 + if (token) headers.set('Authorization', `Bearer ${token}`)
  87 +
  88 + const res = await fetch(path, { ...init, headers })
  89 +
  90 + if (res.status === 401) {
  91 + if (onUnauthorized) onUnauthorized()
  92 + throw new ApiError('Not authenticated', 401, null)
  93 + }
  94 + if (!res.ok) {
  95 + let body: unknown = null
  96 + let message = `${res.status} ${res.statusText}`
  97 + try {
  98 + const text = await res.text()
  99 + if (text) {
  100 + try {
  101 + body = JSON.parse(text)
  102 + const m = (body as { message?: unknown }).message
  103 + if (typeof m === 'string') message = m
  104 + } catch {
  105 + body = text
  106 + message = text
  107 + }
  108 + }
  109 + } catch {
  110 + // ignore body parse errors
  111 + }
  112 + throw new ApiError(message, res.status, body)
  113 + }
  114 + if (!expectJson || res.status === 204) {
  115 + return undefined as T
  116 + }
  117 + return (await res.json()) as T
  118 +}
  119 +
  120 +// ─── Public unauthenticated calls ────────────────────────────────────
  121 +
  122 +export const meta = {
  123 + info: () => apiFetch<MetaInfo>('/api/v1/_meta/info'),
  124 +}
  125 +
  126 +export const auth = {
  127 + login: (username: string, password: string) =>
  128 + apiFetch<TokenPair>('/api/v1/auth/login', {
  129 + method: 'POST',
  130 + body: JSON.stringify({ username, password }),
  131 + }),
  132 +}
  133 +
  134 +// ─── Catalog ─────────────────────────────────────────────────────────
  135 +
  136 +export const catalog = {
  137 + listItems: () => apiFetch<Item[]>('/api/v1/catalog/items'),
  138 + getItem: (id: string) => apiFetch<Item>(`/api/v1/catalog/items/${id}`),
  139 + listUoms: () => apiFetch<Uom[]>('/api/v1/catalog/uoms'),
  140 +}
  141 +
  142 +// ─── Partners ────────────────────────────────────────────────────────
  143 +
  144 +export const partners = {
  145 + list: () => apiFetch<Partner[]>('/api/v1/partners/partners'),
  146 + get: (id: string) => apiFetch<Partner>(`/api/v1/partners/partners/${id}`),
  147 +}
  148 +
  149 +// ─── Inventory ───────────────────────────────────────────────────────
  150 +
  151 +export const inventory = {
  152 + listLocations: () => apiFetch<Location[]>('/api/v1/inventory/locations'),
  153 + listBalances: () => apiFetch<StockBalance[]>('/api/v1/inventory/balances'),
  154 + listMovements: () => apiFetch<StockMovement[]>('/api/v1/inventory/movements'),
  155 +}
  156 +
  157 +// ─── Sales orders ────────────────────────────────────────────────────
  158 +
  159 +export const salesOrders = {
  160 + list: () => apiFetch<SalesOrder[]>('/api/v1/orders/sales-orders'),
  161 + get: (id: string) => apiFetch<SalesOrder>(`/api/v1/orders/sales-orders/${id}`),
  162 + confirm: (id: string) =>
  163 + apiFetch<SalesOrder>(`/api/v1/orders/sales-orders/${id}/confirm`, {
  164 + method: 'POST',
  165 + }),
  166 + cancel: (id: string) =>
  167 + apiFetch<SalesOrder>(`/api/v1/orders/sales-orders/${id}/cancel`, {
  168 + method: 'POST',
  169 + }),
  170 + ship: (id: string, shippingLocationCode: string) =>
  171 + apiFetch<SalesOrder>(`/api/v1/orders/sales-orders/${id}/ship`, {
  172 + method: 'POST',
  173 + body: JSON.stringify({ shippingLocationCode }),
  174 + }),
  175 +}
  176 +
  177 +// ─── Purchase orders ─────────────────────────────────────────────────
  178 +
  179 +export const purchaseOrders = {
  180 + list: () => apiFetch<PurchaseOrder[]>('/api/v1/orders/purchase-orders'),
  181 + get: (id: string) => apiFetch<PurchaseOrder>(`/api/v1/orders/purchase-orders/${id}`),
  182 + confirm: (id: string) =>
  183 + apiFetch<PurchaseOrder>(`/api/v1/orders/purchase-orders/${id}/confirm`, {
  184 + method: 'POST',
  185 + }),
  186 + cancel: (id: string) =>
  187 + apiFetch<PurchaseOrder>(`/api/v1/orders/purchase-orders/${id}/cancel`, {
  188 + method: 'POST',
  189 + }),
  190 + receive: (id: string, receivingLocationCode: string) =>
  191 + apiFetch<PurchaseOrder>(`/api/v1/orders/purchase-orders/${id}/receive`, {
  192 + method: 'POST',
  193 + body: JSON.stringify({ receivingLocationCode }),
  194 + }),
  195 +}
  196 +
  197 +// ─── Production ──────────────────────────────────────────────────────
  198 +
  199 +export const production = {
  200 + listWorkOrders: () => apiFetch<WorkOrder[]>('/api/v1/production/work-orders'),
  201 + getWorkOrder: (id: string) =>
  202 + apiFetch<WorkOrder>(`/api/v1/production/work-orders/${id}`),
  203 + startWorkOrder: (id: string) =>
  204 + apiFetch<WorkOrder>(`/api/v1/production/work-orders/${id}/start`, {
  205 + method: 'POST',
  206 + }),
  207 + completeWorkOrder: (id: string, outputLocationCode: string) =>
  208 + apiFetch<WorkOrder>(`/api/v1/production/work-orders/${id}/complete`, {
  209 + method: 'POST',
  210 + body: JSON.stringify({ outputLocationCode }),
  211 + }),
  212 + cancelWorkOrder: (id: string) =>
  213 + apiFetch<WorkOrder>(`/api/v1/production/work-orders/${id}/cancel`, {
  214 + method: 'POST',
  215 + }),
  216 + shopFloor: () =>
  217 + apiFetch<ShopFloorEntry[]>('/api/v1/production/work-orders/shop-floor'),
  218 +}
  219 +
  220 +// ─── Finance ─────────────────────────────────────────────────────────
  221 +
  222 +export const finance = {
  223 + listJournalEntries: () =>
  224 + apiFetch<JournalEntry[]>('/api/v1/finance/journal-entries'),
  225 +}
... ...
web/src/auth/AuthContext.tsx 0 → 100644
  1 +// vibe_erp web auth context.
  2 +//
  3 +// **Scope.** Holds the in-memory copy of the access token + the user
  4 +// it was issued to (decoded from the JWT payload), exposes login /
  5 +// logout actions, and rerenders subscribers when the token state
  6 +// flips. Persistence lives in localStorage via the api client's
  7 +// `getAccessToken/setAccessToken` helpers — the context only mirrors
  8 +// what's in storage so a hard refresh of the page picks up the
  9 +// existing session without a re-login.
  10 +//
  11 +// **Why decode the JWT in the SPA.** vibe_erp's auth tokens are
  12 +// signed (HS256) by the framework, so the SPA cannot *trust* the
  13 +// payload — but it can DISPLAY it (username, role list) without a
  14 +// round-trip. Anything load-bearing (permission checks) still
  15 +// happens server-side. The decode is intentionally trivial: split
  16 +// on '.', base64-decode the middle segment, JSON.parse. No JWT lib
  17 +// dependency; an invalid token just degrades to "unknown user" and
  18 +// the next API call gets a 401, triggering a logout.
  19 +//
  20 +// **401 handling.** The api client calls registerUnauthorizedHandler
  21 +// once at boot. When any request returns 401, the handler clears
  22 +// the token and forces a navigate to /login (preserving the
  23 +// attempted path so post-login can redirect back).
  24 +
  25 +import {
  26 + createContext,
  27 + useCallback,
  28 + useContext,
  29 + useEffect,
  30 + useMemo,
  31 + useState,
  32 + type ReactNode,
  33 +} from 'react'
  34 +import { useLocation, useNavigate } from 'react-router-dom'
  35 +import {
  36 + auth as authApi,
  37 + getAccessToken,
  38 + registerUnauthorizedHandler,
  39 + setAccessToken,
  40 +} from '@/api/client'
  41 +
  42 +interface JwtPayload {
  43 + sub?: string
  44 + username?: string
  45 + roles?: string[]
  46 + exp?: number
  47 +}
  48 +
  49 +function decodeJwt(token: string): JwtPayload | null {
  50 + try {
  51 + const parts = token.split('.')
  52 + if (parts.length !== 3) return null
  53 + const padded = parts[1] + '==='.slice((parts[1].length + 3) % 4)
  54 + const json = atob(padded.replace(/-/g, '+').replace(/_/g, '/'))
  55 + return JSON.parse(json) as JwtPayload
  56 + } catch {
  57 + return null
  58 + }
  59 +}
  60 +
  61 +interface AuthState {
  62 + token: string | null
  63 + username: string | null
  64 + roles: string[]
  65 + loading: boolean
  66 + login: (username: string, password: string) => Promise<void>
  67 + logout: () => void
  68 +}
  69 +
  70 +const AuthContext = createContext<AuthState | null>(null)
  71 +
  72 +export function AuthProvider({ children }: { children: ReactNode }) {
  73 + const navigate = useNavigate()
  74 + const location = useLocation()
  75 + const [token, setToken] = useState<string | null>(() => getAccessToken())
  76 + const [loading, setLoading] = useState(false)
  77 +
  78 + const payload = useMemo(() => (token ? decodeJwt(token) : null), [token])
  79 +
  80 + const logout = useCallback(() => {
  81 + setAccessToken(null)
  82 + setToken(null)
  83 + if (location.pathname !== '/login') {
  84 + navigate('/login', {
  85 + replace: true,
  86 + state: { from: location.pathname + location.search },
  87 + })
  88 + }
  89 + }, [navigate, location])
  90 +
  91 + // Wire the api client's 401 handler exactly once.
  92 + useEffect(() => {
  93 + registerUnauthorizedHandler(() => {
  94 + // Defer to next tick so the throw inside apiFetch isn't
  95 + // swallowed by React's render-phase complaints.
  96 + setTimeout(() => logout(), 0)
  97 + })
  98 + }, [logout])
  99 +
  100 + const login = useCallback(
  101 + async (username: string, password: string) => {
  102 + setLoading(true)
  103 + try {
  104 + const pair = await authApi.login(username, password)
  105 + setAccessToken(pair.accessToken)
  106 + setToken(pair.accessToken)
  107 + } finally {
  108 + setLoading(false)
  109 + }
  110 + },
  111 + [],
  112 + )
  113 +
  114 + const value: AuthState = {
  115 + token,
  116 + username: payload?.username ?? payload?.sub ?? null,
  117 + roles: payload?.roles ?? [],
  118 + loading,
  119 + login,
  120 + logout,
  121 + }
  122 +
  123 + return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
  124 +}
  125 +
  126 +export function useAuth(): AuthState {
  127 + const ctx = useContext(AuthContext)
  128 + if (!ctx) throw new Error('useAuth must be used inside <AuthProvider>')
  129 + return ctx
  130 +}
... ...
web/src/components/DataTable.tsx 0 → 100644
  1 +// Tiny generic table component.
  2 +//
  3 +// Each column declares: a header label, a key, and an optional
  4 +// `render` function that takes the row and returns a ReactNode.
  5 +// Without `render` the column reads `row[key]` and stringifies it.
  6 +//
  7 +// Intentionally not a heavy table lib (TanStack Table, AG Grid).
  8 +// v1 SPA needs columnar reads, link cells, status badges, and
  9 +// filtering by a free-text input — all trivially handcoded. A
  10 +// future chunk can swap this out the day a real consumer asks for
  11 +// virtualised rows or column resizing.
  12 +
  13 +import type { ReactNode } from 'react'
  14 +
  15 +export interface Column<T> {
  16 + header: string
  17 + key: string
  18 + render?: (row: T) => ReactNode
  19 + className?: string
  20 +}
  21 +
  22 +interface Props<T> {
  23 + rows: T[]
  24 + columns: Column<T>[]
  25 + empty?: ReactNode
  26 + rowKey?: (row: T) => string
  27 +}
  28 +
  29 +export function DataTable<T>({
  30 + rows,
  31 + columns,
  32 + empty = <div className="p-6 text-sm text-slate-400">No rows.</div>,
  33 + rowKey,
  34 +}: Props<T>) {
  35 + if (rows.length === 0) {
  36 + return <div className="card">{empty}</div>
  37 + }
  38 + return (
  39 + <div className="card overflow-x-auto">
  40 + <table className="table-base">
  41 + <thead className="bg-slate-50">
  42 + <tr>
  43 + {columns.map((c) => (
  44 + <th key={c.key} className={c.className}>
  45 + {c.header}
  46 + </th>
  47 + ))}
  48 + </tr>
  49 + </thead>
  50 + <tbody className="divide-y divide-slate-100">
  51 + {rows.map((row, idx) => {
  52 + const key = rowKey
  53 + ? rowKey(row)
  54 + : ((row as { id?: string }).id ?? String(idx))
  55 + return (
  56 + <tr key={key} className="hover:bg-slate-50">
  57 + {columns.map((c) => (
  58 + <td key={c.key} className={c.className}>
  59 + {c.render
  60 + ? c.render(row)
  61 + : ((row as unknown as Record<string, unknown>)[c.key] as ReactNode) ?? ''}
  62 + </td>
  63 + ))}
  64 + </tr>
  65 + )
  66 + })}
  67 + </tbody>
  68 + </table>
  69 + </div>
  70 + )
  71 +}
... ...
web/src/components/ErrorBox.tsx 0 → 100644
  1 +interface Props {
  2 + error: unknown
  3 +}
  4 +
  5 +export function ErrorBox({ error }: Props) {
  6 + let message: string
  7 + if (error instanceof Error) {
  8 + message = error.message
  9 + } else if (typeof error === 'string') {
  10 + message = error
  11 + } else if (error && typeof error === 'object' && 'message' in error) {
  12 + message = String((error as { message: unknown }).message)
  13 + } else {
  14 + message = 'Unknown error'
  15 + }
  16 + return (
  17 + <div className="rounded-md border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800">
  18 + <span className="font-semibold">Error:</span> {message}
  19 + </div>
  20 + )
  21 +}
... ...
web/src/components/Loading.tsx 0 → 100644
  1 +export function Loading({ label = 'Loading…' }: { label?: string }) {
  2 + return (
  3 + <div className="flex items-center gap-2 text-sm text-slate-500">
  4 + <span className="inline-block h-3 w-3 animate-pulse rounded-full bg-brand-500" />
  5 + {label}
  6 + </div>
  7 + )
  8 +}
... ...
web/src/components/PageHeader.tsx 0 → 100644
  1 +import type { ReactNode } from 'react'
  2 +
  3 +interface Props {
  4 + title: string
  5 + subtitle?: string
  6 + actions?: ReactNode
  7 +}
  8 +
  9 +export function PageHeader({ title, subtitle, actions }: Props) {
  10 + return (
  11 + <div className="mb-6 flex flex-col items-start gap-3 sm:flex-row sm:items-end sm:justify-between">
  12 + <div>
  13 + <h1 className="text-2xl font-semibold text-slate-900">{title}</h1>
  14 + {subtitle && <p className="mt-1 text-sm text-slate-500">{subtitle}</p>}
  15 + </div>
  16 + {actions && <div className="flex flex-wrap items-center gap-2">{actions}</div>}
  17 + </div>
  18 + )
  19 +}
... ...
web/src/components/ProtectedRoute.tsx 0 → 100644
  1 +import { Navigate, useLocation } from 'react-router-dom'
  2 +import { useAuth } from '@/auth/AuthContext'
  3 +import type { ReactNode } from 'react'
  4 +
  5 +export function ProtectedRoute({ children }: { children: ReactNode }) {
  6 + const { token } = useAuth()
  7 + const location = useLocation()
  8 + if (!token) {
  9 + return (
  10 + <Navigate
  11 + to="/login"
  12 + replace
  13 + state={{ from: location.pathname + location.search }}
  14 + />
  15 + )
  16 + }
  17 + return <>{children}</>
  18 +}
... ...
web/src/components/StatusBadge.tsx 0 → 100644
  1 +interface Props {
  2 + status: string
  3 +}
  4 +
  5 +// Status → Tailwind class via the shared @layer components classes
  6 +// declared in index.css. Falls back to 'badge-pending' for unknown
  7 +// statuses so a new server-side enum value renders as a neutral pill
  8 +// instead of throwing.
  9 +export function StatusBadge({ status }: Props) {
  10 + const cls =
  11 + {
  12 + DRAFT: 'badge-draft',
  13 + CONFIRMED: 'badge-confirmed',
  14 + SHIPPED: 'badge-shipped',
  15 + RECEIVED: 'badge-received',
  16 + COMPLETED: 'badge-completed',
  17 + CANCELLED: 'badge-cancelled',
  18 + IN_PROGRESS: 'badge-in-progress',
  19 + PENDING: 'badge-pending',
  20 + POSTED: 'badge-confirmed',
  21 + SETTLED: 'badge-shipped',
  22 + REVERSED: 'badge-cancelled',
  23 + }[status] ?? 'badge-pending'
  24 + return <span className={cls}>{status}</span>
  25 +}
... ...
web/src/index.css 0 → 100644
  1 +@tailwind base;
  2 +@tailwind components;
  3 +@tailwind utilities;
  4 +
  5 +:root {
  6 + font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  7 + -webkit-font-smoothing: antialiased;
  8 + -moz-osx-font-smoothing: grayscale;
  9 +}
  10 +
  11 +@layer components {
  12 + .btn {
  13 + @apply inline-flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium shadow-sm transition focus:outline-none focus:ring-2 focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50;
  14 + }
  15 + .btn-primary {
  16 + @apply btn bg-brand-500 text-white hover:bg-brand-600 focus:ring-brand-500;
  17 + }
  18 + .btn-secondary {
  19 + @apply btn bg-white text-slate-800 ring-1 ring-inset ring-slate-300 hover:bg-slate-50 focus:ring-brand-500;
  20 + }
  21 + .btn-danger {
  22 + @apply btn bg-rose-600 text-white hover:bg-rose-700 focus:ring-rose-600;
  23 + }
  24 + .card {
  25 + @apply rounded-lg border border-slate-200 bg-white shadow-sm;
  26 + }
  27 + .table-base {
  28 + @apply min-w-full divide-y divide-slate-200 text-sm;
  29 + }
  30 + .table-base th {
  31 + @apply px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-slate-500;
  32 + }
  33 + .table-base td {
  34 + @apply whitespace-nowrap px-3 py-2 text-slate-700;
  35 + }
  36 + .badge {
  37 + @apply inline-flex items-center rounded-full px-2 py-0.5 text-xs font-semibold;
  38 + }
  39 + .badge-draft { @apply badge bg-slate-100 text-slate-700; }
  40 + .badge-confirmed { @apply badge bg-amber-100 text-amber-800; }
  41 + .badge-shipped { @apply badge bg-emerald-100 text-emerald-800; }
  42 + .badge-received { @apply badge bg-emerald-100 text-emerald-800; }
  43 + .badge-completed { @apply badge bg-emerald-100 text-emerald-800; }
  44 + .badge-cancelled { @apply badge bg-rose-100 text-rose-800; }
  45 + .badge-in-progress { @apply badge bg-sky-100 text-sky-800; }
  46 + .badge-pending { @apply badge bg-slate-100 text-slate-600; }
  47 +}
... ...
web/src/layout/AppLayout.tsx 0 → 100644
  1 +// vibe_erp main app shell.
  2 +//
  3 +// Top bar (logo + version + user + logout) and a permanent left
  4 +// sidebar grouped by Packaged Business Capability (PBC). The
  5 +// `<Outlet />` renders whichever child route the router matched.
  6 +//
  7 +// **Why a static nav array.** The framework already exposes
  8 +// menu metadata at /api/v1/_meta/metadata, but plumbing that into
  9 +// the SPA is a Phase 3 (Tier 1 customization) concern. v1 SPA
  10 +// hard-codes a sensible default that mirrors the implemented PBCs;
  11 +// a future chunk replaces this with a metadata-driven sidebar so
  12 +// plug-ins can contribute new menus through their YAML.
  13 +
  14 +import { NavLink, Outlet } from 'react-router-dom'
  15 +import { useEffect, useState } from 'react'
  16 +import { useAuth } from '@/auth/AuthContext'
  17 +import { meta } from '@/api/client'
  18 +import type { MetaInfo } from '@/types/api'
  19 +
  20 +interface NavItem {
  21 + to: string
  22 + label: string
  23 +}
  24 +interface NavGroup {
  25 + heading: string
  26 + items: NavItem[]
  27 +}
  28 +
  29 +const NAV: NavGroup[] = [
  30 + {
  31 + heading: 'Overview',
  32 + items: [{ to: '/', label: 'Dashboard' }],
  33 + },
  34 + {
  35 + heading: 'Catalog & Partners',
  36 + items: [
  37 + { to: '/items', label: 'Items' },
  38 + { to: '/uoms', label: 'Units of Measure' },
  39 + { to: '/partners', label: 'Partners' },
  40 + ],
  41 + },
  42 + {
  43 + heading: 'Inventory',
  44 + items: [
  45 + { to: '/locations', label: 'Locations' },
  46 + { to: '/balances', label: 'Stock Balances' },
  47 + { to: '/movements', label: 'Stock Movements' },
  48 + ],
  49 + },
  50 + {
  51 + heading: 'Orders',
  52 + items: [
  53 + { to: '/sales-orders', label: 'Sales Orders' },
  54 + { to: '/purchase-orders', label: 'Purchase Orders' },
  55 + ],
  56 + },
  57 + {
  58 + heading: 'Production',
  59 + items: [
  60 + { to: '/work-orders', label: 'Work Orders' },
  61 + { to: '/shop-floor', label: 'Shop Floor' },
  62 + ],
  63 + },
  64 + {
  65 + heading: 'Finance',
  66 + items: [{ to: '/journal-entries', label: 'Journal Entries' }],
  67 + },
  68 +]
  69 +
  70 +export function AppLayout() {
  71 + const { username, logout } = useAuth()
  72 + const [info, setInfo] = useState<MetaInfo | null>(null)
  73 +
  74 + useEffect(() => {
  75 + meta.info().then(setInfo).catch(() => setInfo(null))
  76 + }, [])
  77 +
  78 + return (
  79 + <div className="flex min-h-screen bg-slate-50 text-slate-900">
  80 + {/* ─── Sidebar ──────────────────────────────────────────── */}
  81 + <aside className="hidden w-60 flex-shrink-0 border-r border-slate-200 bg-white sm:flex sm:flex-col">
  82 + <div className="flex h-14 items-center px-4 border-b border-slate-200">
  83 + <span className="text-lg font-bold text-brand-600">vibe_erp</span>
  84 + {info && (
  85 + <span className="ml-2 text-xs text-slate-400">
  86 + {info.implementationVersion}
  87 + </span>
  88 + )}
  89 + </div>
  90 + <nav className="flex-1 overflow-y-auto px-3 py-4">
  91 + {NAV.map((group) => (
  92 + <div key={group.heading} className="mb-5">
  93 + <div className="px-2 text-xs font-semibold uppercase tracking-wider text-slate-400">
  94 + {group.heading}
  95 + </div>
  96 + <ul className="mt-1 space-y-0.5">
  97 + {group.items.map((item) => (
  98 + <li key={item.to}>
  99 + <NavLink
  100 + to={item.to}
  101 + end={item.to === '/'}
  102 + className={({ isActive }) =>
  103 + [
  104 + 'block rounded-md px-2 py-1.5 text-sm transition',
  105 + isActive
  106 + ? 'bg-brand-50 text-brand-700 font-semibold'
  107 + : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900',
  108 + ].join(' ')
  109 + }
  110 + >
  111 + {item.label}
  112 + </NavLink>
  113 + </li>
  114 + ))}
  115 + </ul>
  116 + </div>
  117 + ))}
  118 + </nav>
  119 + <div className="border-t border-slate-200 px-3 py-3 text-xs text-slate-400">
  120 + {info?.activeProfiles?.length ? (
  121 + <div>profiles: {info.activeProfiles.join(', ')}</div>
  122 + ) : null}
  123 + {info?.buildTime ? (
  124 + <div>built: {new Date(info.buildTime).toLocaleString()}</div>
  125 + ) : null}
  126 + </div>
  127 + </aside>
  128 +
  129 + {/* ─── Main column ─────────────────────────────────────── */}
  130 + <div className="flex flex-1 flex-col">
  131 + <header className="flex h-14 items-center justify-between border-b border-slate-200 bg-white px-6 sm:justify-end">
  132 + <span className="text-lg font-bold text-brand-600 sm:hidden">vibe_erp</span>
  133 + <div className="flex items-center gap-3">
  134 + <span className="text-sm text-slate-500">
  135 + {username ? (
  136 + <>
  137 + Signed in as <span className="font-medium text-slate-700">{username}</span>
  138 + </>
  139 + ) : (
  140 + 'Signed in'
  141 + )}
  142 + </span>
  143 + <button className="btn-secondary" onClick={logout}>
  144 + Logout
  145 + </button>
  146 + </div>
  147 + </header>
  148 + <main className="flex-1 overflow-y-auto p-6">
  149 + <Outlet />
  150 + </main>
  151 + </div>
  152 + </div>
  153 + )
  154 +}
... ...
web/src/main.tsx 0 → 100644
  1 +import React from 'react'
  2 +import ReactDOM from 'react-dom/client'
  3 +import { BrowserRouter } from 'react-router-dom'
  4 +import App from './App'
  5 +import { AuthProvider } from './auth/AuthContext'
  6 +import './index.css'
  7 +
  8 +ReactDOM.createRoot(document.getElementById('root')!).render(
  9 + <React.StrictMode>
  10 + <BrowserRouter>
  11 + <AuthProvider>
  12 + <App />
  13 + </AuthProvider>
  14 + </BrowserRouter>
  15 + </React.StrictMode>,
  16 +)
... ...
web/src/pages/BalancesPage.tsx 0 → 100644
  1 +import { useEffect, useState } from 'react'
  2 +import { inventory } from '@/api/client'
  3 +import type { Location, StockBalance } from '@/types/api'
  4 +import { PageHeader } from '@/components/PageHeader'
  5 +import { Loading } from '@/components/Loading'
  6 +import { ErrorBox } from '@/components/ErrorBox'
  7 +import { DataTable, type Column } from '@/components/DataTable'
  8 +
  9 +interface Row extends Record<string, unknown> {
  10 + id: string
  11 + itemCode: string
  12 + locationCode: string
  13 + quantity: string | number
  14 +}
  15 +
  16 +export function BalancesPage() {
  17 + const [rows, setRows] = useState<Row[]>([])
  18 + const [error, setError] = useState<Error | null>(null)
  19 + const [loading, setLoading] = useState(true)
  20 +
  21 + useEffect(() => {
  22 + Promise.all([inventory.listBalances(), inventory.listLocations()])
  23 + .then(([balances, locs]: [StockBalance[], Location[]]) => {
  24 + const byId = new Map(locs.map((l) => [l.id, l.code]))
  25 + setRows(
  26 + balances.map((b) => ({
  27 + id: b.id,
  28 + itemCode: b.itemCode,
  29 + locationCode: byId.get(b.locationId) ?? b.locationId,
  30 + quantity: b.quantity,
  31 + })),
  32 + )
  33 + })
  34 + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e))))
  35 + .finally(() => setLoading(false))
  36 + }, [])
  37 +
  38 + const columns: Column<Row>[] = [
  39 + { header: 'Item', key: 'itemCode', render: (r) => <span className="font-mono">{r.itemCode}</span> },
  40 + {
  41 + header: 'Location',
  42 + key: 'locationCode',
  43 + render: (r) => <span className="font-mono">{r.locationCode}</span>,
  44 + },
  45 + {
  46 + header: 'Quantity',
  47 + key: 'quantity',
  48 + render: (r) => <span className="font-mono tabular-nums">{String(r.quantity)}</span>,
  49 + },
  50 + ]
  51 +
  52 + return (
  53 + <div>
  54 + <PageHeader
  55 + title="Stock Balances"
  56 + subtitle="On-hand quantities per (item, location). Updates atomically with every movement."
  57 + />
  58 + {loading && <Loading />}
  59 + {error && <ErrorBox error={error} />}
  60 + {!loading && !error && <DataTable rows={rows} columns={columns} />}
  61 + </div>
  62 + )
  63 +}
... ...
web/src/pages/DashboardPage.tsx 0 → 100644
  1 +import { useEffect, useState } from 'react'
  2 +import { Link } from 'react-router-dom'
  3 +import {
  4 + catalog,
  5 + finance,
  6 + inventory,
  7 + partners,
  8 + production,
  9 + purchaseOrders,
  10 + salesOrders,
  11 +} from '@/api/client'
  12 +import { PageHeader } from '@/components/PageHeader'
  13 +import { Loading } from '@/components/Loading'
  14 +import { ErrorBox } from '@/components/ErrorBox'
  15 +import { useAuth } from '@/auth/AuthContext'
  16 +
  17 +interface DashboardCounts {
  18 + items: number
  19 + partners: number
  20 + locations: number
  21 + salesOrders: number
  22 + purchaseOrders: number
  23 + workOrders: number
  24 + journalEntries: number
  25 + inProgressWorkOrders: number
  26 +}
  27 +
  28 +export function DashboardPage() {
  29 + const { username } = useAuth()
  30 + const [counts, setCounts] = useState<DashboardCounts | null>(null)
  31 + const [error, setError] = useState<Error | null>(null)
  32 + const [loading, setLoading] = useState(true)
  33 +
  34 + useEffect(() => {
  35 + let active = true
  36 + setLoading(true)
  37 + Promise.all([
  38 + catalog.listItems(),
  39 + partners.list(),
  40 + inventory.listLocations(),
  41 + salesOrders.list(),
  42 + purchaseOrders.list(),
  43 + production.listWorkOrders(),
  44 + finance.listJournalEntries(),
  45 + production.shopFloor(),
  46 + ])
  47 + .then(([items, parts, locs, sos, pos, wos, jes, sf]) => {
  48 + if (!active) return
  49 + setCounts({
  50 + items: items.length,
  51 + partners: parts.length,
  52 + locations: locs.length,
  53 + salesOrders: sos.length,
  54 + purchaseOrders: pos.length,
  55 + workOrders: wos.length,
  56 + journalEntries: jes.length,
  57 + inProgressWorkOrders: sf.length,
  58 + })
  59 + })
  60 + .catch((e: unknown) => {
  61 + if (active) setError(e instanceof Error ? e : new Error(String(e)))
  62 + })
  63 + .finally(() => active && setLoading(false))
  64 + return () => {
  65 + active = false
  66 + }
  67 + }, [])
  68 +
  69 + return (
  70 + <div>
  71 + <PageHeader
  72 + title={`Welcome${username ? ', ' + username : ''}`}
  73 + subtitle="The framework's buy-make-sell loop, end to end through the same Postgres."
  74 + />
  75 + {loading && <Loading />}
  76 + {error && <ErrorBox error={error} />}
  77 + {counts && (
  78 + <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
  79 + <DashboardCard label="Items" value={counts.items} to="/items" />
  80 + <DashboardCard label="Partners" value={counts.partners} to="/partners" />
  81 + <DashboardCard label="Locations" value={counts.locations} to="/locations" />
  82 + <DashboardCard
  83 + label="Work orders in progress"
  84 + value={counts.inProgressWorkOrders}
  85 + to="/shop-floor"
  86 + highlight
  87 + />
  88 + <DashboardCard label="Sales orders" value={counts.salesOrders} to="/sales-orders" />
  89 + <DashboardCard
  90 + label="Purchase orders"
  91 + value={counts.purchaseOrders}
  92 + to="/purchase-orders"
  93 + />
  94 + <DashboardCard label="Work orders" value={counts.workOrders} to="/work-orders" />
  95 + <DashboardCard
  96 + label="Journal entries"
  97 + value={counts.journalEntries}
  98 + to="/journal-entries"
  99 + />
  100 + </div>
  101 + )}
  102 + <div className="mt-8 card p-5 text-sm text-slate-600">
  103 + <h2 className="mb-2 text-base font-semibold text-slate-800">Try the demo</h2>
  104 + <ol className="list-decimal space-y-1 pl-5">
  105 + <li>
  106 + Open a <Link to="/sales-orders" className="text-brand-600 hover:underline">sales order</Link>{' '}
  107 + in DRAFT, click <span className="font-mono">Confirm</span>.
  108 + </li>
  109 + <li>
  110 + With the same sales order CONFIRMED, click <span className="font-mono">Ship</span>.
  111 + The framework atomically debits stock, flips status to SHIPPED, and emits a domain
  112 + event.
  113 + </li>
  114 + <li>
  115 + Watch <Link to="/balances" className="text-brand-600 hover:underline">stock balances</Link>{' '}
  116 + drop, <Link to="/movements" className="text-brand-600 hover:underline">movements</Link>{' '}
  117 + grow by one row, and{' '}
  118 + <Link to="/journal-entries" className="text-brand-600 hover:underline">
  119 + journal entries
  120 + </Link>{' '}
  121 + settle from POSTED to SETTLED — all from the cross-PBC event subscriber in pbc-finance.
  122 + </li>
  123 + </ol>
  124 + </div>
  125 + </div>
  126 + )
  127 +}
  128 +
  129 +function DashboardCard({
  130 + label,
  131 + value,
  132 + to,
  133 + highlight = false,
  134 +}: {
  135 + label: string
  136 + value: number
  137 + to: string
  138 + highlight?: boolean
  139 +}) {
  140 + return (
  141 + <Link
  142 + to={to}
  143 + className={[
  144 + 'card flex flex-col gap-1 p-5 transition hover:shadow-md',
  145 + highlight ? 'ring-2 ring-brand-200' : '',
  146 + ].join(' ')}
  147 + >
  148 + <div className="text-xs font-semibold uppercase tracking-wide text-slate-500">{label}</div>
  149 + <div className="text-3xl font-bold text-slate-900">{value}</div>
  150 + </Link>
  151 + )
  152 +}
... ...
web/src/pages/ItemsPage.tsx 0 → 100644
  1 +import { useEffect, useState } from 'react'
  2 +import { catalog } from '@/api/client'
  3 +import type { Item } from '@/types/api'
  4 +import { PageHeader } from '@/components/PageHeader'
  5 +import { Loading } from '@/components/Loading'
  6 +import { ErrorBox } from '@/components/ErrorBox'
  7 +import { DataTable, type Column } from '@/components/DataTable'
  8 +
  9 +export function ItemsPage() {
  10 + const [rows, setRows] = useState<Item[]>([])
  11 + const [error, setError] = useState<Error | null>(null)
  12 + const [loading, setLoading] = useState(true)
  13 +
  14 + useEffect(() => {
  15 + catalog
  16 + .listItems()
  17 + .then(setRows)
  18 + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e))))
  19 + .finally(() => setLoading(false))
  20 + }, [])
  21 +
  22 + const columns: Column<Item>[] = [
  23 + { header: 'Code', key: 'code', render: (r) => <span className="font-mono">{r.code}</span> },
  24 + { header: 'Name', key: 'name' },
  25 + { header: 'Type', key: 'itemType' },
  26 + { header: 'UoM', key: 'baseUomCode', render: (r) => <span className="font-mono">{r.baseUomCode}</span> },
  27 + {
  28 + header: 'Active',
  29 + key: 'active',
  30 + render: (r) => (r.active ? <span className="text-emerald-600">●</span> : <span className="text-slate-300">●</span>),
  31 + },
  32 + ]
  33 +
  34 + return (
  35 + <div>
  36 + <PageHeader
  37 + title="Items"
  38 + subtitle="Catalog of items the framework can transact: raw materials, components, finished goods, services."
  39 + />
  40 + {loading && <Loading />}
  41 + {error && <ErrorBox error={error} />}
  42 + {!loading && !error && <DataTable rows={rows} columns={columns} />}
  43 + </div>
  44 + )
  45 +}
... ...
web/src/pages/JournalEntriesPage.tsx 0 → 100644
  1 +import { useEffect, useState } from 'react'
  2 +import { finance } from '@/api/client'
  3 +import type { JournalEntry } from '@/types/api'
  4 +import { PageHeader } from '@/components/PageHeader'
  5 +import { Loading } from '@/components/Loading'
  6 +import { ErrorBox } from '@/components/ErrorBox'
  7 +import { DataTable, type Column } from '@/components/DataTable'
  8 +import { StatusBadge } from '@/components/StatusBadge'
  9 +
  10 +export function JournalEntriesPage() {
  11 + const [rows, setRows] = useState<JournalEntry[]>([])
  12 + const [error, setError] = useState<Error | null>(null)
  13 + const [loading, setLoading] = useState(true)
  14 +
  15 + useEffect(() => {
  16 + finance
  17 + .listJournalEntries()
  18 + .then((rs) =>
  19 + setRows([...rs].sort((a, b) => (b.postedAt ?? '').localeCompare(a.postedAt ?? ''))),
  20 + )
  21 + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e))))
  22 + .finally(() => setLoading(false))
  23 + }, [])
  24 +
  25 + const columns: Column<JournalEntry>[] = [
  26 + {
  27 + header: 'Posted',
  28 + key: 'postedAt',
  29 + render: (r) => (r.postedAt ? new Date(r.postedAt).toLocaleString() : '—'),
  30 + },
  31 + { header: 'Code', key: 'code', render: (r) => <span className="font-mono text-xs">{r.code}</span> },
  32 + { header: 'Type', key: 'type' },
  33 + { header: 'Status', key: 'status', render: (r) => <StatusBadge status={r.status} /> },
  34 + { header: 'Order', key: 'orderCode', render: (r) => <span className="font-mono">{r.orderCode}</span> },
  35 + { header: 'Partner', key: 'partnerCode', render: (r) => <span className="font-mono">{r.partnerCode}</span> },
  36 + {
  37 + header: 'Amount',
  38 + key: 'amount',
  39 + render: (r) => (
  40 + <span className="font-mono tabular-nums">
  41 + {Number(r.amount).toLocaleString(undefined, { minimumFractionDigits: 2 })}{' '}
  42 + {r.currencyCode}
  43 + </span>
  44 + ),
  45 + },
  46 + ]
  47 +
  48 + return (
  49 + <div>
  50 + <PageHeader
  51 + title="Journal Entries"
  52 + subtitle="AR / AP entries created reactively by pbc-finance from order lifecycle events."
  53 + />
  54 + {loading && <Loading />}
  55 + {error && <ErrorBox error={error} />}
  56 + {!loading && !error && <DataTable rows={rows} columns={columns} />}
  57 + </div>
  58 + )
  59 +}
... ...
web/src/pages/LocationsPage.tsx 0 → 100644
  1 +import { useEffect, useState } from 'react'
  2 +import { inventory } from '@/api/client'
  3 +import type { Location } from '@/types/api'
  4 +import { PageHeader } from '@/components/PageHeader'
  5 +import { Loading } from '@/components/Loading'
  6 +import { ErrorBox } from '@/components/ErrorBox'
  7 +import { DataTable, type Column } from '@/components/DataTable'
  8 +
  9 +export function LocationsPage() {
  10 + const [rows, setRows] = useState<Location[]>([])
  11 + const [error, setError] = useState<Error | null>(null)
  12 + const [loading, setLoading] = useState(true)
  13 +
  14 + useEffect(() => {
  15 + inventory
  16 + .listLocations()
  17 + .then(setRows)
  18 + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e))))
  19 + .finally(() => setLoading(false))
  20 + }, [])
  21 +
  22 + const columns: Column<Location>[] = [
  23 + { header: 'Code', key: 'code', render: (r) => <span className="font-mono">{r.code}</span> },
  24 + { header: 'Name', key: 'name' },
  25 + { header: 'Type', key: 'type' },
  26 + {
  27 + header: 'Active',
  28 + key: 'active',
  29 + render: (r) => (r.active ? <span className="text-emerald-600">●</span> : <span className="text-slate-300">●</span>),
  30 + },
  31 + ]
  32 +
  33 + return (
  34 + <div>
  35 + <PageHeader title="Locations" subtitle="Warehouses, bins, and virtual locations the inventory PBC tracks." />
  36 + {loading && <Loading />}
  37 + {error && <ErrorBox error={error} />}
  38 + {!loading && !error && <DataTable rows={rows} columns={columns} />}
  39 + </div>
  40 + )
  41 +}
... ...
web/src/pages/LoginPage.tsx 0 → 100644
  1 +import { useEffect, useState, type FormEvent } from 'react'
  2 +import { useLocation, useNavigate } from 'react-router-dom'
  3 +import { useAuth } from '@/auth/AuthContext'
  4 +import { meta } from '@/api/client'
  5 +import type { MetaInfo } from '@/types/api'
  6 +import { ErrorBox } from '@/components/ErrorBox'
  7 +
  8 +interface LocationState {
  9 + from?: string
  10 +}
  11 +
  12 +export function LoginPage() {
  13 + const { login, token, loading } = useAuth()
  14 + const navigate = useNavigate()
  15 + const location = useLocation()
  16 + const [username, setUsername] = useState('admin')
  17 + const [password, setPassword] = useState('')
  18 + const [error, setError] = useState<Error | null>(null)
  19 + const [info, setInfo] = useState<MetaInfo | null>(null)
  20 +
  21 + useEffect(() => {
  22 + meta.info().then(setInfo).catch(() => setInfo(null))
  23 + }, [])
  24 +
  25 + // If already logged in (e.g. nav back to /login), bounce to dashboard.
  26 + useEffect(() => {
  27 + if (token) {
  28 + const dest = (location.state as LocationState | null)?.from ?? '/'
  29 + navigate(dest, { replace: true })
  30 + }
  31 + }, [token, navigate, location.state])
  32 +
  33 + const onSubmit = async (e: FormEvent) => {
  34 + e.preventDefault()
  35 + setError(null)
  36 + try {
  37 + await login(username, password)
  38 + // Effect above handles the redirect; nothing else to do here.
  39 + } catch (err: unknown) {
  40 + setError(err instanceof Error ? err : new Error(String(err)))
  41 + }
  42 + }
  43 +
  44 + return (
  45 + <div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-slate-100 to-brand-50 p-6">
  46 + <div className="w-full max-w-md">
  47 + <div className="mb-6 text-center">
  48 + <h1 className="text-3xl font-bold text-brand-600">vibe_erp</h1>
  49 + <p className="mt-1 text-sm text-slate-500">
  50 + Composable ERP framework for the printing industry
  51 + </p>
  52 + </div>
  53 + <div className="card p-6">
  54 + <form onSubmit={onSubmit} className="space-y-4">
  55 + <div>
  56 + <label className="block text-sm font-medium text-slate-700">Username</label>
  57 + <input
  58 + type="text"
  59 + value={username}
  60 + onChange={(e) => setUsername(e.target.value)}
  61 + className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-brand-500 focus:ring-brand-500"
  62 + required
  63 + autoFocus
  64 + />
  65 + </div>
  66 + <div>
  67 + <label className="block text-sm font-medium text-slate-700">Password</label>
  68 + <input
  69 + type="password"
  70 + value={password}
  71 + onChange={(e) => setPassword(e.target.value)}
  72 + className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-brand-500 focus:ring-brand-500"
  73 + required
  74 + />
  75 + <p className="mt-1 text-xs text-slate-400">
  76 + The bootstrap admin password is printed to the application boot log on first start.
  77 + </p>
  78 + </div>
  79 + {error ? <ErrorBox error={error} /> : null}
  80 + <button type="submit" className="btn-primary w-full" disabled={loading}>
  81 + {loading ? 'Signing in…' : 'Sign in'}
  82 + </button>
  83 + </form>
  84 + </div>
  85 + <p className="mt-4 text-center text-xs text-slate-400">
  86 + {info ? (
  87 + <>
  88 + Connected to <span className="font-mono">{info.name}</span>{' '}
  89 + <span className="font-mono">{info.implementationVersion}</span>
  90 + </>
  91 + ) : (
  92 + 'Connecting…'
  93 + )}
  94 + </p>
  95 + </div>
  96 + </div>
  97 + )
  98 +}
... ...
web/src/pages/MovementsPage.tsx 0 → 100644
  1 +import { useEffect, useState } from 'react'
  2 +import { inventory } from '@/api/client'
  3 +import type { Location, StockMovement } from '@/types/api'
  4 +import { PageHeader } from '@/components/PageHeader'
  5 +import { Loading } from '@/components/Loading'
  6 +import { ErrorBox } from '@/components/ErrorBox'
  7 +import { DataTable, type Column } from '@/components/DataTable'
  8 +
  9 +interface Row extends Record<string, unknown> {
  10 + id: string
  11 + occurredAt: string
  12 + itemCode: string
  13 + locationCode: string
  14 + delta: string | number
  15 + reason: string
  16 + reference: string | null
  17 +}
  18 +
  19 +export function MovementsPage() {
  20 + const [rows, setRows] = useState<Row[]>([])
  21 + const [error, setError] = useState<Error | null>(null)
  22 + const [loading, setLoading] = useState(true)
  23 +
  24 + useEffect(() => {
  25 + Promise.all([inventory.listMovements(), inventory.listLocations()])
  26 + .then(([moves, locs]: [StockMovement[], Location[]]) => {
  27 + const byId = new Map(locs.map((l) => [l.id, l.code]))
  28 + const sorted = [...moves].sort((a, b) =>
  29 + (b.occurredAt ?? '').localeCompare(a.occurredAt ?? ''),
  30 + )
  31 + setRows(
  32 + sorted.map((m) => ({
  33 + id: m.id,
  34 + occurredAt: m.occurredAt,
  35 + itemCode: m.itemCode,
  36 + locationCode: byId.get(m.locationId) ?? m.locationId,
  37 + delta: m.delta,
  38 + reason: m.reason,
  39 + reference: m.reference,
  40 + })),
  41 + )
  42 + })
  43 + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e))))
  44 + .finally(() => setLoading(false))
  45 + }, [])
  46 +
  47 + const columns: Column<Row>[] = [
  48 + {
  49 + header: 'Occurred',
  50 + key: 'occurredAt',
  51 + render: (r) =>
  52 + r.occurredAt ? new Date(r.occurredAt).toLocaleString() : '—',
  53 + },
  54 + { header: 'Item', key: 'itemCode', render: (r) => <span className="font-mono">{r.itemCode}</span> },
  55 + { header: 'Location', key: 'locationCode', render: (r) => <span className="font-mono">{r.locationCode}</span> },
  56 + {
  57 + header: 'Δ',
  58 + key: 'delta',
  59 + render: (r) => {
  60 + const n = Number(r.delta)
  61 + const cls = n >= 0 ? 'text-emerald-600' : 'text-rose-600'
  62 + return <span className={`font-mono tabular-nums ${cls}`}>{String(r.delta)}</span>
  63 + },
  64 + },
  65 + { header: 'Reason', key: 'reason' },
  66 + { header: 'Reference', key: 'reference', render: (r) => r.reference ?? '—' },
  67 + ]
  68 +
  69 + return (
  70 + <div>
  71 + <PageHeader
  72 + title="Stock Movements"
  73 + subtitle="Append-only ledger of every change to inventory. Most-recent first."
  74 + />
  75 + {loading && <Loading />}
  76 + {error && <ErrorBox error={error} />}
  77 + {!loading && !error && <DataTable rows={rows} columns={columns} />}
  78 + </div>
  79 + )
  80 +}
... ...
web/src/pages/PartnersPage.tsx 0 → 100644
  1 +import { useEffect, useState } from 'react'
  2 +import { partners } from '@/api/client'
  3 +import type { Partner } from '@/types/api'
  4 +import { PageHeader } from '@/components/PageHeader'
  5 +import { Loading } from '@/components/Loading'
  6 +import { ErrorBox } from '@/components/ErrorBox'
  7 +import { DataTable, type Column } from '@/components/DataTable'
  8 +
  9 +export function PartnersPage() {
  10 + const [rows, setRows] = useState<Partner[]>([])
  11 + const [error, setError] = useState<Error | null>(null)
  12 + const [loading, setLoading] = useState(true)
  13 +
  14 + useEffect(() => {
  15 + partners
  16 + .list()
  17 + .then(setRows)
  18 + .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e))))
  19 + .finally(() => setLoading(false))
  20 + }, [])
  21 +
  22 + const columns: Column<Partner>[] = [
  23 + { header: 'Code', key: 'code', render: (r) => <span className="font-mono">{r.code}</span> },
  24 + { header: 'Name', key: 'name' },
  25 + { header: 'Type', key: 'type' },
  26 + { header: 'Email', key: 'email', render: (r) => r.email ?? '—' },
  27 + { header: 'Phone', key: 'phone', render: (r) => r.phone ?? '—' },
  28 + {
  29 + header: 'Active',
  30 + key: 'active',
  31 + render: (r) => (r.active ? <span className="text-emerald-600">●</span> : <span className="text-slate-300">●</span>),
  32 + },
  33 + ]
  34 +
  35 + return (
  36 + <div>
  37 + <PageHeader
  38 + title="Partners"
  39 + subtitle="Customers, suppliers, and partners that play both roles. Seeded by the demo."
  40 + />
  41 + {loading && <Loading />}
  42 + {error && <ErrorBox error={error} />}
  43 + {!loading && !error && <DataTable rows={rows} columns={columns} />}
  44 + </div>
  45 + )
  46 +}
... ...