From e7015552573ddd3856eab579b288bac5d5ac0816 Mon Sep 17 00:00:00 2001 From: Snow Lee Date: Mon, 13 Jul 2026 09:57:14 -0700 Subject: [PATCH] feat(webapp): [phase 0] React+TS frontend scaffold, build pipeline, e2e harness - Vite+React+TypeScript workspace at internal/webapp/frontend; vite build emits committed assets into internal/webapp/static (the go:embed target), so plain 'go build' still needs no Node - style.css and the SVG sprite ported verbatim; boot layer (api/config, 401->login redirect) ported so the auth flow works end to end - frontend(): content-hashed assets/* now served immutable; index.html stays no-cache (TestFrontendSPAFallback covers both) - committed e2e harness (BDRIVE_E2E_SERVE=1, deterministic seeded hub on :8993) wired as Playwright webServer; 4 shell specs green - check-dist.sh guards against stale committed build output Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt --- docs/react-migration-prd.md | 37 +- internal/webapp/dir_test.go | 22 +- internal/webapp/e2e_serve_test.go | 168 ++ internal/webapp/frontend/.gitignore | 3 + internal/webapp/frontend/check-dist.sh | 14 + internal/webapp/frontend/e2e/helpers.ts | 16 + .../webapp/frontend/e2e/playwright.config.ts | 22 + internal/webapp/frontend/e2e/shell.spec.ts | 31 + internal/webapp/frontend/index.html | 43 + internal/webapp/frontend/package-lock.json | 1972 +++++++++++++++ internal/webapp/frontend/package.json | 25 + internal/webapp/frontend/src/App.tsx | 37 + internal/webapp/frontend/src/api/http.ts | 28 + internal/webapp/frontend/src/api/types.ts | 32 + .../webapp/frontend/src/hooks/useConfig.ts | 24 + internal/webapp/frontend/src/main.tsx | 22 + .../webapp/{static => frontend/src}/style.css | 0 internal/webapp/frontend/tsconfig.json | 17 + internal/webapp/frontend/vite.config.ts | 23 + internal/webapp/server.go | 12 +- internal/webapp/static/app.js | 2156 ----------------- .../webapp/static/assets/index-CZjUv2DS.js | 11 + .../webapp/static/assets/index-oVhdizP9.css | 1 + internal/webapp/static/index.html | 52 +- 24 files changed, 2540 insertions(+), 2228 deletions(-) create mode 100644 internal/webapp/e2e_serve_test.go create mode 100644 internal/webapp/frontend/.gitignore create mode 100755 internal/webapp/frontend/check-dist.sh create mode 100644 internal/webapp/frontend/e2e/helpers.ts create mode 100644 internal/webapp/frontend/e2e/playwright.config.ts create mode 100644 internal/webapp/frontend/e2e/shell.spec.ts create mode 100644 internal/webapp/frontend/index.html create mode 100644 internal/webapp/frontend/package-lock.json create mode 100644 internal/webapp/frontend/package.json create mode 100644 internal/webapp/frontend/src/App.tsx create mode 100644 internal/webapp/frontend/src/api/http.ts create mode 100644 internal/webapp/frontend/src/api/types.ts create mode 100644 internal/webapp/frontend/src/hooks/useConfig.ts create mode 100644 internal/webapp/frontend/src/main.tsx rename internal/webapp/{static => frontend/src}/style.css (100%) create mode 100644 internal/webapp/frontend/tsconfig.json create mode 100644 internal/webapp/frontend/vite.config.ts delete mode 100644 internal/webapp/static/app.js create mode 100644 internal/webapp/static/assets/index-CZjUv2DS.js create mode 100644 internal/webapp/static/assets/index-oVhdizP9.css diff --git a/docs/react-migration-prd.md b/docs/react-migration-prd.md index f141684..1c01982 100644 --- a/docs/react-migration-prd.md +++ b/docs/react-migration-prd.md @@ -100,19 +100,25 @@ refresh; invalidate after uploads/renames/admin actions — mirror today's ## Phases ### Phase 0 — scaffold & pipeline -- [ ] Vite+React+TS workspace at `internal/webapp/frontend/`; build lands in - `internal/webapp/static/` and is committed; old files removed only in - Phase 5 (until then keep building the new app; parity gate flips the - switch — see note below). -- [ ] NOTE: to keep `main`-mergeable states shippable, do the whole - migration on ONE branch; each phase is a commit (or a few), and the - committed `static/` on the branch is the React build from Phase 0 on. -- [ ] `style.css` ported verbatim as global stylesheet; SVG sprite ported. -- [ ] Dev proxy works against a local hub. -- [ ] Cache-header change in `frontend()` + assets served hashed. -- [ ] `check-dist.sh` works. -- [ ] e2e harness (§Verification) committed and starts a seeded hub. -- [ ] `go build ./...`, `go vet ./...`, `go test ./...` green. +- [x] Vite+React+TS workspace at `internal/webapp/frontend/`; build lands in + `internal/webapp/static/` and is committed. The branch's `static/` is + the React build from Phase 0 on (the old files live on `main` / + `git show main:internal/webapp/static/app.js` for porting reference); + the parity gate in Phase 5 is what makes the branch mergeable. +- [x] `style.css` ported verbatim as global stylesheet; SVG sprite ported + (kept inline in `index.html`, as before). +- [x] Dev proxy works against a local hub (`BDRIVE_DEV_PROXY` overrides the + `localhost:8080` default). +- [x] Cache-header change in `frontend()` + assets served hashed + (covered by `TestFrontendSPAFallback`). +- [x] `check-dist.sh` works. +- [x] e2e harness committed (`e2e_serve_test.go`, gated by + `BDRIVE_E2E_SERVE=1`, port 8993, fresh state each run) and wired as + Playwright's webServer; 4 shell specs green. Also ported in Phase 0 + (the e2e login flow needed it): the `getJSON`/`postJSON` layer with + 401→login redirect, `/api/config` boot, and the title/vault-name + wiring (`src/api/http.ts`, `src/hooks/useConfig.ts`). +- [x] `go build ./...`, `go vet ./...`, `go test ./...` green. ### Phase 1 — shell: boot, session, projects, routing - [ ] Boot from `/api/config`; volume vs hub mode both render. @@ -129,6 +135,9 @@ refresh; invalidate after uploads/renames/admin actions — mirror today's - [ ] Breadcrumbs; per-route scroll restoration. - [ ] Download + raw file view; share button state; share dialog. - [ ] Drag-drop upload (presign and relay paths) with refresh after commit. +- [ ] Command palette (⌘K: file names, projects, actions — port the + palette overlay from the old shell) and the topbar overflow menu + (`more-btn`/`more-menu`) for narrow viewports. ### Phase 3 — project home, insights, history - [ ] Project home at `/`: connect guide (3 tabs: "Claude Code & @@ -206,7 +215,7 @@ file path; reload on `/insights`. ## Status -- [ ] Phase 0 +- [x] Phase 0 - [ ] Phase 1 - [ ] Phase 2 - [ ] Phase 3 diff --git a/internal/webapp/dir_test.go b/internal/webapp/dir_test.go index 4cb15bf..9fcb7e2 100644 --- a/internal/webapp/dir_test.go +++ b/internal/webapp/dir_test.go @@ -2,6 +2,7 @@ package webapp import ( "encoding/json" + "io/fs" "net/http" "os" "path/filepath" @@ -78,9 +79,12 @@ func TestFrontendSPAFallback(t *testing.T) { if rec.Code != 200 || !strings.Contains(rec.Header().Get("Content-Type"), "text/html") { t.Fatalf("%s: want 200 html, got %d %s", url, rec.Code, rec.Header().Get("Content-Type")) } - if !strings.Contains(rec.Body.String(), `id="sidebar"`) { + if !strings.Contains(rec.Body.String(), `id="root"`) { t.Fatalf("%s: expected the app shell, got %.60q", url, rec.Body.String()) } + if cc := rec.Header().Get("Cache-Control"); cc != "no-cache" { + t.Fatalf("%s: the shell must revalidate, got Cache-Control %q", url, cc) + } } // Client routes all resolve to the shell, not a 404 or file content. shell("/") @@ -88,12 +92,18 @@ func TestFrontendSPAFallback(t *testing.T) { shell("/p-deadbeef/notes/plan.md") // hub-style route shell("/join/abc123") // invite route - // Real assets are served as themselves. - if rec := get(t, h, "/app.js"); rec.Code != 200 || !strings.Contains(rec.Header().Get("Content-Type"), "javascript") { - t.Fatalf("/app.js: %d %s", rec.Code, rec.Header().Get("Content-Type")) + // Real assets are served as themselves, cacheable forever: Vite emits + // content-hashed filenames, so find one instead of hardcoding a hash. + assets, err := fs.Glob(staticFiles, "static/assets/*.js") + if err != nil || len(assets) == 0 { + t.Fatalf("no built js asset embedded (run npm run build in frontend/): %v", err) } - if rec := get(t, h, "/style.css"); rec.Code != 200 || !strings.Contains(rec.Header().Get("Content-Type"), "css") { - t.Fatalf("/style.css: %d %s", rec.Code, rec.Header().Get("Content-Type")) + rec := get(t, h, strings.TrimPrefix(assets[0], "static")) + if rec.Code != 200 || !strings.Contains(rec.Header().Get("Content-Type"), "javascript") { + t.Fatalf("%s: %d %s", assets[0], rec.Code, rec.Header().Get("Content-Type")) + } + if cc := rec.Header().Get("Cache-Control"); !strings.Contains(cc, "immutable") { + t.Fatalf("hashed assets must be immutable, got Cache-Control %q", cc) } // A mistyped API path is a genuine 404, not the shell. diff --git a/internal/webapp/e2e_serve_test.go b/internal/webapp/e2e_serve_test.go new file mode 100644 index 0000000..c0c06c4 --- /dev/null +++ b/internal/webapp/e2e_serve_test.go @@ -0,0 +1,168 @@ +package webapp + +// E2E hub harness for the frontend's Playwright suite (frontend/e2e). Not +// part of the normal test suite: it only runs with BDRIVE_E2E_SERVE=1, where +// it serves a small deterministic hub on :8993 until killed. Playwright +// starts it via its webServer config and tears it down after the run. +// +// Unlike the manual demo harness, state is wiped on every start so the +// tests always see the same world: one org ("default", owned by the admin +// account), one project ("wiki") with a handful of seeded files, two +// accounts, one share-ready markdown tree with wikilinks, and enough read +// heat to light up the insights views. + +import ( + "crypto/sha256" + "encoding/hex" + "net/http" + "os" + "path/filepath" + "testing" + "time" + + "github.com/runbear-io/beardrive/internal/journal" + "github.com/runbear-io/beardrive/internal/remote" +) + +const ( + e2eAddr = "0.0.0.0:8993" + e2eAdmin = "e2e@example.com" + e2eMember = "member@example.com" + e2ePassword = "e2e-pass-1" +) + +func TestE2EServe(t *testing.T) { + if os.Getenv("BDRIVE_E2E_SERVE") == "" { + t.Skip("frontend e2e harness; set BDRIVE_E2E_SERVE=1 to run") + } + state := filepath.Join(os.TempDir(), "bdrive-e2e-hub") + if err := os.RemoveAll(state); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(state, "storage"), 0o755); err != nil { + t.Fatal(err) + } + + be, err := remote.Open(t.Context(), "file://"+filepath.Join(state, "storage")) + if err != nil { + t.Fatal(err) + } + db, err := OpenProjectDB(filepath.Join(state, "projects.json")) + if err != nil { + t.Fatal(err) + } + p, _, err := db.GetOrCreate("wiki", "") + if err != nil { + t.Fatal(err) + } + srv := &Server{Root: be, Projects: db, Device: webDevice, Refresh: 0, Upload: UploadConfig{Enabled: true}} + + seedE2E(t, state, filepath.Join(state, "storage", p.ID), p.ID) + + srv.Reads, err = OpenReadLedger(filepath.Join(state, "reads.json"), 0) + if err != nil { + t.Fatal(err) + } + srv.Devices, _ = OpenDeviceRegistry(filepath.Join(state, "devices.json")) + srv.Devices.Observe(DeviceInfo{ID: "seed", Name: "seed-agent", OS: "linux/amd64"}) + + auth, err := OpenBuiltinAuth(filepath.Join(state, "auth.json"), false, nil) + if err != nil { + t.Fatal(err) + } + if _, err := auth.signup(e2eAdmin, "E2E Admin", e2ePassword); err != nil { + t.Fatal(err) + } + if _, err := auth.signup(e2eMember, "E2E Member", e2ePassword); err != nil { + t.Fatal(err) + } + auth.Admins = map[string]bool{e2eAdmin: true} + srv.Auth = auth + + orgs, err := OpenOrgDB(filepath.Join(state, "orgs.json")) + if err != nil { + t.Fatal(err) + } + org, err := orgs.Create("default", e2eAdmin) + if err != nil { + t.Fatal(err) + } + if err := orgs.AddMember(org.ID, e2eMember, RoleMember); err != nil { + t.Fatal(err) + } + if err := db.SetOrg(p.ID, org.ID); err != nil { + t.Fatal(err) + } + srv.Orgs = orgs + auth.InviteValid = orgs.ValidInvite + + shares, err := OpenShareDB(filepath.Join(state, "shares.json")) + if err != nil { + t.Fatal(err) + } + srv.Shares = shares + + t.Logf("e2e hub on http://%s (state: %s) — %s / %s", e2eAddr, state, e2eAdmin, e2ePassword) + go http.ListenAndServe(e2eAddr, srv.Handler()) + time.Sleep(2 * time.Hour) // Playwright kills the process when the run ends +} + +// seedE2E journals a small fixed file tree (with history on one path and one +// binary) and read heat for it. +func seedE2E(t *testing.T, state, prefix, projectID string) { + t.Helper() + os.MkdirAll(filepath.Join(prefix, "journal"), 0o755) + os.MkdirAll(filepath.Join(prefix, "blobs"), 0o755) + now := time.Now().UTC() + var ops []journal.Op + var lam, seq int64 + put := func(path, content string, age time.Duration) { + sum := sha256.Sum256([]byte(content)) + blob := hex.EncodeToString(sum[:]) + if err := os.WriteFile(filepath.Join(prefix, "blobs", blob), []byte(content), 0o644); err != nil { + t.Fatal(err) + } + lam++ + seq++ + ops = append(ops, journal.Op{ + Seq: seq, Lamport: lam, Time: now.Add(-age), + Device: "seed", DeviceName: "seed-agent", Author: "alice@x.io", + User: "alice@x.io", UserName: "Alice", + Kind: journal.KindPut, Path: path, Blob: blob, + Size: int64(len(content)), Mode: 0o644, + }) + } + put("index.md", "# Wiki\n\nStart at the [[guide]] or browse [notes](notes/readme.md).\n", 72*time.Hour) + put("guide.md", "# Guide\n\nFirst version of the guide.\n", 48*time.Hour) + put("guide.md", "# Guide\n\nSecond version of the guide, with more detail.\n", 2*time.Hour) + put("notes/readme.md", "# Notes\n\nNested folder content.\n", 24*time.Hour) + put("notes/deep/topic.md", "# Topic\n\nDeeply nested file.\n", 24*time.Hour) + // Tiny valid PNG (1x1), enough to exercise the binary/download path. + png := "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89" + + "\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82" + put("assets/logo.png", png, 24*time.Hour) + if err := journal.Append(filepath.Join(prefix, "journal", "seed.jsonl"), ops); err != nil { + t.Fatal(err) + } + + day := now.Format("2006-01-02") + var stats []ReadStat + for _, rd := range []struct { + path string + kind string + actor string + n int64 + }{ + {"index.md", ReadKindHuman, "alice@x.io", 12}, + {"index.md", ReadKindAgent, "seed", 30}, + {"guide.md", ReadKindHuman, "bob@x.io", 5}, + {"guide.md", ReadKindAgent, "seed", 9}, + {"notes/readme.md", ReadKindAgent, "seed", 2}, + } { + stats = append(stats, ReadStat{Project: projectID, Path: rd.path, Day: day, + Kind: rd.kind, Actor: rd.actor, Count: rd.n, Last: now}) + } + if err := newFileReadRepo(filepath.Join(state, "reads.json")).PutBatch(stats); err != nil { + t.Fatal(err) + } +} diff --git a/internal/webapp/frontend/.gitignore b/internal/webapp/frontend/.gitignore new file mode 100644 index 0000000..dbd64df --- /dev/null +++ b/internal/webapp/frontend/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +test-results/ +playwright-report/ diff --git a/internal/webapp/frontend/check-dist.sh b/internal/webapp/frontend/check-dist.sh new file mode 100755 index 0000000..ae39709 --- /dev/null +++ b/internal/webapp/frontend/check-dist.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# Verify the committed build output at ../static matches the frontend +# source. Run before tagging a release (goreleaser requires a clean tree, +# so a stale dist would otherwise slip through silently). +set -e +cd "$(dirname "$0")" +npm ci +npm run build +if [ -n "$(git status --porcelain -- ../static)" ]; then + echo "internal/webapp/static is stale: rebuild and commit it" >&2 + git status --short -- ../static >&2 + exit 1 +fi +echo "internal/webapp/static is fresh" diff --git a/internal/webapp/frontend/e2e/helpers.ts b/internal/webapp/frontend/e2e/helpers.ts new file mode 100644 index 0000000..4cd46f6 --- /dev/null +++ b/internal/webapp/frontend/e2e/helpers.ts @@ -0,0 +1,16 @@ +import { Page } from "@playwright/test"; + +export const ADMIN = "e2e@example.com"; +export const MEMBER = "member@example.com"; +export const PASSWORD = "e2e-pass-1"; + +// Signs in through the server-rendered /auth pages and waits for the SPA +// shell to render. +export async function login(page: Page, email: string = ADMIN) { + await page.goto("/"); + await page.waitForURL(/auth\/login/); + await page.fill('input[name="email"]', email); + await page.fill('input[name="password"]', PASSWORD); + await page.click("form button"); + await page.waitForSelector("#sidebar"); +} diff --git a/internal/webapp/frontend/e2e/playwright.config.ts b/internal/webapp/frontend/e2e/playwright.config.ts new file mode 100644 index 0000000..9f474cb --- /dev/null +++ b/internal/webapp/frontend/e2e/playwright.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from "@playwright/test"; + +// The suite runs against the committed Go e2e harness (e2e_serve_test.go): +// a deterministic seeded hub on :8993, freshly wiped on every start. It +// exercises the BUILT frontend served by the Go binary — run `npm run +// build` before `npm run e2e`, or test against stale assets. +export default defineConfig({ + testDir: ".", + timeout: 30_000, + retries: 0, + workers: 1, // specs share one hub with mutable state (uploads, shares) + use: { + baseURL: "http://localhost:8993", + }, + webServer: { + command: + "cd ../../../.. && BDRIVE_E2E_SERVE=1 go test -count=1 -timeout 3h -run TestE2EServe ./internal/webapp", + url: "http://localhost:8993/", + reuseExistingServer: true, + timeout: 60_000, + }, +}); diff --git a/internal/webapp/frontend/e2e/shell.spec.ts b/internal/webapp/frontend/e2e/shell.spec.ts new file mode 100644 index 0000000..99beaf1 --- /dev/null +++ b/internal/webapp/frontend/e2e/shell.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from "@playwright/test"; +import { login } from "./helpers"; + +test("unauthenticated visit redirects to the login page", async ({ page }) => { + await page.goto("/"); + await page.waitForURL(/auth\/login/); + await expect(page.locator('input[name="email"]')).toBeVisible(); +}); + +test("login lands in the app shell", async ({ page }) => { + await login(page); + await expect(page).toHaveTitle("BearDrive"); + await expect(page.locator("#sidebar")).toBeVisible(); + await expect(page.locator("#topbar")).toBeVisible(); +}); + +test("hashed assets are served immutable, shell revalidates", async ({ page, request }) => { + await login(page); + const src = await page.locator('script[type="module"]').getAttribute("src"); + expect(src).toMatch(/^\/assets\/.+\.js$/); + const asset = await request.get(src!); + expect(asset.headers()["cache-control"]).toContain("immutable"); + const shell = await request.get("/"); + expect(shell.headers()["cache-control"]).toBe("no-cache"); +}); + +test("mistyped API path is a real 404, not the shell", async ({ page }) => { + await login(page); // unauthenticated /api/* is a 401 before routing + const res = await page.request.get("/api/nope"); + expect(res.status()).toBe(404); +}); diff --git a/internal/webapp/frontend/index.html b/internal/webapp/frontend/index.html new file mode 100644 index 0000000..50e2df7 --- /dev/null +++ b/internal/webapp/frontend/index.html @@ -0,0 +1,43 @@ + + + + + +BearDrive + + + + +
+ + + diff --git a/internal/webapp/frontend/package-lock.json b/internal/webapp/frontend/package-lock.json new file mode 100644 index 0000000..e3abb46 --- /dev/null +++ b/internal/webapp/frontend/package-lock.json @@ -0,0 +1,1972 @@ +{ + "name": "beardrive-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "beardrive-frontend", + "version": "0.0.0", + "dependencies": { + "@tanstack/react-query": "^5.90.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.9.0" + }, + "devDependencies": { + "@playwright/test": "1.61.1", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.9.0", + "vite": "^7.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.2.tgz", + "integrity": "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.2.tgz", + "integrity": "sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001805", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", + "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.389", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", + "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/internal/webapp/frontend/package.json b/internal/webapp/frontend/package.json new file mode 100644 index 0000000..7db8bec --- /dev/null +++ b/internal/webapp/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "beardrive-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "e2e": "playwright test -c e2e" + }, + "dependencies": { + "@tanstack/react-query": "^5.90.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.9.0" + }, + "devDependencies": { + "@playwright/test": "1.61.1", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.9.0", + "vite": "^7.1.0" + } +} diff --git a/internal/webapp/frontend/src/App.tsx b/internal/webapp/frontend/src/App.tsx new file mode 100644 index 0000000..1983dd2 --- /dev/null +++ b/internal/webapp/frontend/src/App.tsx @@ -0,0 +1,37 @@ +import { useEffect } from "react"; +import { useConfig } from "./hooks/useConfig"; + +// Phase 0 shell: boots /api/config (redirecting to /auth/login when there +// is no session) and renders the static layout so the ported stylesheet can +// be verified. Routing and data views arrive in Phase 1. +export default function App() { + const { data: config } = useConfig(); + + useEffect(() => { + if (config) document.title = config.brand || config.volume || "BearDrive"; + }, [config]); + + return ( + <> +
+ +
+
+ + +
+
+
{config ? "Select a file to read it." : "Loading…"}
+
+
+ + ); +} diff --git a/internal/webapp/frontend/src/api/http.ts b/internal/webapp/frontend/src/api/http.ts new file mode 100644 index 0000000..b214eca --- /dev/null +++ b/internal/webapp/frontend/src/api/http.ts @@ -0,0 +1,28 @@ +// Fetch helpers shared by every API hook. All URLs are root-absolute: a +// deep path like /// must never break relative +// resolution. + +function toLogin(): never { + // Auth required: sign in, then come back to the current route. + location.href = + "/auth/login?next=" + encodeURIComponent(location.pathname + location.search); + throw new Error("signing in…"); +} + +export async function getJSON(url: string): Promise { + const r = await fetch(url); + if (r.status === 401) toLogin(); + if (!r.ok) throw new Error(await r.text()); + return r.json(); +} + +export async function postJSON(url: string, body?: unknown): Promise { + const r = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body || {}), + }); + if (r.status === 401) toLogin(); + if (!r.ok) throw new Error(await r.text()); + return r.json(); +} diff --git a/internal/webapp/frontend/src/api/types.ts b/internal/webapp/frontend/src/api/types.ts new file mode 100644 index 0000000..2f624fe --- /dev/null +++ b/internal/webapp/frontend/src/api/types.ts @@ -0,0 +1,32 @@ +// Hand-written TypeScript shapes for the Go API responses. Derived from the +// handler structs in internal/webapp — keep in sync when handlers change. +// The API is deliberately storage-blind: nothing here ever names a bucket, +// remote URL, or credential, and heat responses carry no actor identities. + +// GET /api/config (handleConfig, server.go) +export interface ServerConfig { + mode: "volume" | "hub"; + volume: string; + brand: string; + upload: { enabled: boolean }; + auth: { + enabled: boolean; + cli_login?: string; + allow_signup?: boolean; + admin?: boolean; + }; + reads: { enabled: boolean }; + me?: { email: string; name: string }; +} + +// GET /api/projects (handleProjectList → Project, projects.go) +export interface Project { + id: string; + name: string; + org?: string; + created?: string; +} + +export interface ProjectList { + projects: Project[]; +} diff --git a/internal/webapp/frontend/src/hooks/useConfig.ts b/internal/webapp/frontend/src/hooks/useConfig.ts new file mode 100644 index 0000000..f2352ff --- /dev/null +++ b/internal/webapp/frontend/src/hooks/useConfig.ts @@ -0,0 +1,24 @@ +import { useQuery } from "@tanstack/react-query"; +import { getJSON } from "../api/http"; +import type { ServerConfig } from "../api/types"; + +// The first request the app makes; everything else keys off its answer. +// If auth is on and there is no session, redirect to the login page here +// rather than letting every authed call 401 (noisy console, and the +// redirect happens anyway). /api/config reports `me` only when signed in. +export function useConfig() { + return useQuery({ + queryKey: ["config"], + queryFn: async () => { + const cfg = await getJSON("/api/config"); + if (cfg.auth.enabled && !cfg.me) { + location.href = + "/auth/login?next=" + + encodeURIComponent(location.pathname + location.search); + await new Promise(() => {}); // never resolve; we're navigating away + } + return cfg; + }, + staleTime: Infinity, + }); +} diff --git a/internal/webapp/frontend/src/main.tsx b/internal/webapp/frontend/src/main.tsx new file mode 100644 index 0000000..a0b6ef3 --- /dev/null +++ b/internal/webapp/frontend/src/main.tsx @@ -0,0 +1,22 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import App from "./App"; +import "./style.css"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: 1, refetchOnWindowFocus: false }, + }, +}); + +createRoot(document.getElementById("root")!).render( + + + + + + + , +); diff --git a/internal/webapp/static/style.css b/internal/webapp/frontend/src/style.css similarity index 100% rename from internal/webapp/static/style.css rename to internal/webapp/frontend/src/style.css diff --git a/internal/webapp/frontend/tsconfig.json b/internal/webapp/frontend/tsconfig.json new file mode 100644 index 0000000..d2a3956 --- /dev/null +++ b/internal/webapp/frontend/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noEmit": true, + "isolatedModules": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/internal/webapp/frontend/vite.config.ts b/internal/webapp/frontend/vite.config.ts new file mode 100644 index 0000000..409a255 --- /dev/null +++ b/internal/webapp/frontend/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// Build output feeds the Go binary: ../static is the go:embed target in +// server.go, and the compiled assets are committed so plain `go build` +// needs no Node. Run ./check-dist.sh to verify the committed output is +// fresh. +// Dev target: a locally running hub (`bdrive web` or the e2e harness). +const target = process.env.BDRIVE_DEV_PROXY || "http://localhost:8080"; + +export default defineConfig({ + plugins: [react()], + build: { outDir: "../static", emptyOutDir: true }, + server: { + proxy: { + // Everything the Go server owns; the frontend itself only ever uses + // root-absolute URLs, so prefix proxying is enough. + "/api": target, + "/auth": target, + "^/s/": target, + }, + }, +}); diff --git a/internal/webapp/server.go b/internal/webapp/server.go index 543d791..dc9f822 100644 --- a/internal/webapp/server.go +++ b/internal/webapp/server.go @@ -383,10 +383,16 @@ func (s *Server) frontend(static fs.FS) http.HandlerFunc { files := http.FileServerFS(static) index, _ := fs.ReadFile(static, "index.html") return func(w http.ResponseWriter, r *http.Request) { - // Embedded assets carry no modtime, so without this browsers cache - // them heuristically and users see a stale frontend after upgrades. - w.Header().Set("Cache-Control", "no-cache") upath := strings.TrimPrefix(path.Clean("/"+r.URL.Path), "/") + // Vite emits content-hashed filenames under assets/, safe to cache + // forever. Everything else (index.html above all) must revalidate: + // embedded files carry no modtime, so without no-cache browsers + // cache heuristically and users see a stale frontend after upgrades. + if strings.HasPrefix(upath, "assets/") { + w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + } else { + w.Header().Set("Cache-Control", "no-cache") + } // Reserved prefixes that fell through to the catch-all are genuine // 404s — don't mask a mistyped API/auth/share URL with the app shell. if strings.HasPrefix(upath, "api/") || strings.HasPrefix(upath, "auth/") || strings.HasPrefix(upath, "s/") { diff --git a/internal/webapp/static/app.js b/internal/webapp/static/app.js deleted file mode 100644 index 34d32ab..0000000 --- a/internal/webapp/static/app.js +++ /dev/null @@ -1,2156 +0,0 @@ -/* beardrive web viewer: file tree + obsidian-like markdown pane. No dependencies. */ -"use strict"; - -const $ = (id) => document.getElementById(id); -let flatFiles = []; // [{path, name}] for wikilink resolution -let dirIndex = new Map(); // dir path → tree node, for folder listings -let currentPath = null; -let expanded = new Set(); // dir paths that are open (folders start closed) -let treeFirstLoad = true; // apply the "lone root folder opens" rule once per project - -const MD_EXT = /\.(md|markdown)$/i; -const IMG_EXT = /\.(png|jpe?g|gif|svg|webp|ico|bmp|avif)$/i; -const TEXT_EXT = /\.(txt|log|json|ya?ml|toml|csv|go|py|js|ts|jsx|tsx|sh|bash|zsh|rb|rs|c|h|cpp|java|kt|swift|sql|html|css|xml|ini|conf|env|mod|sum|jsonl)$/i; - -/* The client is storage-blind: where files live and how uploads reach - storage is the server's business, learned from /api/config. A hub server - hosts many projects; volume-scoped calls go under api/p//. */ -let serverConfig = { mode: "volume", upload: { enabled: false } }; -let projects = []; -let currentProject = null; // hub mode: the selected project -let apiBase = "/api/"; // volume-scoped endpoint prefix -let orgs = []; // hub mode: the orgs this account belongs to -let joinedOrgId = null; // org just joined via an invite this page-load -let heatMap = null; // hub: path → 30-day read counts, from the heat API -let heatAt = 0; // when heatMap was last fetched (ms) - -const fileURL = (p) => apiBase + "file?path=" + encodeURIComponent(p); - -/* inline SVG icon, pulled from the sprite in index.html */ -function svgIcon(name) { return ``; } - -/* Deterministic accent for a project's letter-mark, so each project keeps a - stable color across reloads without any server state. */ -const PROJ_COLORS = ["#5b8def", "#f5a623", "#4cc38a", "#e0679b", "#8b7bf0", "#3ec8c8", "#e6934a"]; -function projColor(s) { - let h = 0; - for (const c of s) h = (h * 31 + c.charCodeAt(0)) >>> 0; - return PROJ_COLORS[h % PROJ_COLORS.length]; -} - -async function getJSON(url) { - const r = await fetch(url); - if (r.status === 401) { // auth required: sign in, then come back here - location.href = "/auth/login?next=" + encodeURIComponent(location.pathname + location.search); - throw new Error("signing in…"); - } - if (!r.ok) throw new Error(await r.text()); - return r.json(); -} - -async function postJSON(url, body) { - const r = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body || {}), - }); - if (r.status === 401) { - location.href = "/auth/login?next=" + encodeURIComponent(location.pathname + location.search); - throw new Error("signing in…"); - } - if (!r.ok) throw new Error(await r.text()); - return r.json(); -} - -/* ---- boot ---- */ -async function boot() { - // The app restores per-route scroll itself (scrollMemo); the browser's own - // restoration would fight it on back/forward. - if ("scrollRestoration" in history) history.scrollRestoration = "manual"; - try { - serverConfig = await getJSON("/api/config"); - } catch { /* non-fatal */ } - document.title = serverConfig.brand || serverConfig.volume || "BearDrive"; - // If auth is on and we're not signed in, go straight to the login page - // rather than firing authed API calls that 401 (noisy console, and the - // redirect happens anyway). /api/config reports `me` only when signed in. - if (serverConfig.auth && serverConfig.auth.enabled && !serverConfig.me) { - location.href = "/auth/login?next=" + encodeURIComponent(location.pathname + location.search); - return; - } - if (serverConfig.auth && serverConfig.auth.enabled) $("signout").hidden = false; - if (serverConfig.mode === "hub") { - await acceptInviteFromURL(); - await loadOrgs(); - await loadProjects(); - updateAdminBar(); - const route = parseRoute(); - // After accepting an invite, open a project in the org you just joined - // rather than whatever happened to be first. - const proj = projects.find((x) => x.id === route.project) - || (joinedOrgId && projects.find((x) => x.org === joinedOrgId)) - || projects[0]; - if (proj) selectProject(proj, route); - else { $("vault-name").textContent = serverConfig.brand || serverConfig.volume || "BearDrive"; updateOrgBar(); showEmptyState(); } - setInterval(loadProjects, 30000); // pick up new projects - } else { - $("vault-name").textContent = serverConfig.volume || "BearDrive"; - initUpload(); - await refreshTree(); - const { path } = parseRoute(); - if (path) openPath(path); - } - setInterval(refreshTree, 15000); // pick up synced changes -} - -/* ---- hub: projects ---- */ -async function loadProjects() { - let out; - try { - out = await getJSON("/api/projects"); - } catch { return; } - projects = out.projects || []; - const nav = $("projects"); - nav.hidden = false; - nav.innerHTML = ""; - const head = document.createElement("div"); - head.className = "nav-head"; - head.innerHTML = `Projects`; - const add = document.createElement("button"); - add.className = "nav-add"; - add.title = "New project"; - add.textContent = "+"; - add.onclick = async () => { - const name = await modalPrompt("New project", "Project name", "", "Create"); - if (name) createProject(name); - }; - head.appendChild(add); - nav.appendChild(head); - const ul = document.createElement("ul"); - for (const p of projects) { - const li = document.createElement("li"); - const row = document.createElement("div"); - row.className = "row" + (currentProject && currentProject.id === p.id ? " active" : ""); - const mark = document.createElement("span"); - mark.className = "proj-mark"; - mark.style.background = projColor(p.name); - mark.textContent = (p.name.trim()[0] || "?"); - const lab = document.createElement("span"); - lab.className = "label"; - lab.textContent = p.name; - row.append(mark, lab); - row.title = p.name; - row.tabIndex = 0; - row.setAttribute("role", "button"); - row.onclick = () => selectProject(p, null); - row.onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); row.click(); } }; - ul.appendChild(li).appendChild(row); - } - nav.appendChild(ul); -} - -function selectProject(p, route) { - currentProject = p; - expanded = new Set(); // fresh collapse state for the new project's tree - treeFirstLoad = true; - heatMap = null; // the old project's heat means nothing here - heatAt = 0; - lastTreeJSON = ""; - lastHeatJSON = ""; - apiBase = "/api/p/" + p.id + "/"; - $("vault-name").textContent = p.name; - // The project name doubles as a "home" link back to the index page. - $("vault-name").classList.add("vault-link"); - $("vault-name").onclick = () => { showProjectHome(); closeSidebarOnMobile(); }; - document.title = p.name + " — BearDrive"; - currentPath = null; - $("crumb").textContent = ""; - $("meta").textContent = ""; - $("download").hidden = true; - $("content").className = "view"; - $("content").innerHTML = `
Loading…
`; - loadProjects(); // refresh active highlight - updateOrgBar(); - initUpload(); - initHistory(); - updateShareButton(); - // Landing view (applyRoute with an empty route): the project home page — - // the connect-an-agent guide, with Insights below for admins/owners. - refreshTree().then(() => applyRoute(route || {})); - if (!route || (!route.path && !route.view)) pushURL("/" + p.id); -} - -/* ---- hub: organizations ---- */ - -/* Opening "/join/" joins the invite's org. If the visitor isn't - signed in yet, postJSON's 401 handler sends them to /auth/login with the - /join path intact in `next`, so after signing in the server re-serves the - app there and the join completes — the token is never lost. */ -async function acceptInviteFromURL() { - const m = location.pathname.match(/^\/join\/([0-9a-f]+)\/?$/); - if (!m) return; - try { - const out = await postJSON("/api/invites/" + m[1]); // may redirect to login (401) - history.replaceState(null, "", "/"); - joinedOrgId = out.org && out.org.id; - toast("Welcome — you joined the “" + out.org.name + "” team. Opening its projects…"); - } catch (e) { - if (String(e.message).includes("signing in")) throw e; // redirecting; stop boot - history.replaceState(null, "", "/"); - toast("Could not accept the invite: " + e.message, true); - } -} - -/* Onboarding: a signed-in account with no projects shouldn't hit a blank - sidebar. Explain that access comes from an invite, let them paste one, - and — since any member can — offer to start a new project. */ -function showEmptyState() { - $("orgbar").hidden = true; - $("content").className = "view"; - const auth = serverConfig.auth && serverConfig.auth.enabled; - $("content").innerHTML = ` -
-

Welcome to BearDrive

-

You're signed in, but you're not part of any project yet.

- ${auth ? ` -
-

Have an invite link?

-

A teammate can send you a join link. Paste it here:

-
- - -
-
` : ``} -
-

Or start a new project

-

Create a shared space for your team's files.

-
- - -
-
-
`; - const join = $("ob-join"); - if (join) join.onclick = () => { - const v = $("ob-invite").value.trim(); - const m = v.match(/join\/([0-9a-f]+)/) || v.match(/^([0-9a-f]{8,})$/); - if (!m) { toast("That doesn't look like an invite link.", true); return; } - location.href = "/join/" + m[1]; - }; - $("ob-create").onclick = () => createProject($("ob-name").value.trim()); -} - -async function createProject(name) { - if (!name) { toast("Give the project a name.", true); return; } - try { - const out = await postJSON("/api/projects", { name }); - await loadOrgs(); - await loadProjects(); - selectProject(out.project, null); - toast("Created “" + out.project.name + "”."); - } catch (e) { - toast("Could not create the project: " + e.message, true); - } -} - -async function loadOrgs() { - try { - orgs = (await getJSON("/api/orgs")).orgs || []; - } catch { orgs = []; } -} - -function currentOrg() { - if (!currentProject || !currentProject.org) return null; - return orgs.find((o) => o.id === currentProject.org) || null; -} - -/* The sidebar footer names the project's org; clicking it lists members, - and owners get an Invite button that mints a join link. */ -function updateOrgBar() { - const bar = $("orgbar"), org = currentOrg(); - // The top-of-sidebar gear is the always-visible admin entry point: any - // account that owns an org (or is a hub admin) gets it, whatever project - // is open. - const owned = orgs.find((o) => o.role === "owner"); - const gear = $("settings-btn"); - if (gear) { - const target = (org && org.role === "owner") ? org : owned; - gear.hidden = !target; - gear.onclick = () => showOrgAdmin(target); - } - if (!org) { bar.hidden = true; return; } - bar.hidden = false; - const nm = $("org-name"); - nm.textContent = org.name; - nm.title = "Manage organization"; - nm.onclick = () => showOrgAdmin(org); - nm.onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); nm.click(); } }; - const btn = $("invite-btn"); - btn.hidden = org.role !== "owner"; - btn.textContent = "Manage"; - btn.onclick = () => showOrgAdmin(org); -} - -/* The org admin panel: members (owners can change roles / remove), rename, - invite links (create / revoke), and an org-wide audit of public shares. */ -async function showOrgAdmin(org) { - currentPath = null; - markActive(); - closeSidebarOnMobile(); - $("crumb").textContent = org.name; - $("meta").textContent = ""; - $("share-btn").hidden = $("history-btn").hidden = $("download").hidden = $("more-btn").hidden = true; - const owner = org.role === "owner"; - const box = $("content"); - box.className = "view"; - box.innerHTML = `

`; - box.querySelector("#org-title").textContent = org.name + (owner ? "" : " · member"); - const panel = box.querySelector(".admin"); - - if (owner) { - const rn = el(panel, "div", "admin-row"); - rn.innerHTML = ``; - rn.querySelector("#org-rename").value = org.name; - rn.querySelector("#org-rename-btn").onclick = async () => { - const name = rn.querySelector("#org-rename").value.trim(); - try { await api("PATCH", "/api/orgs/" + org.id, { name }); toast("Renamed."); await loadOrgs(); refreshAll(); } - catch (e) { toast(e.message, true); } - }; - } - - // Members - el(panel, "h3", null, "Members"); - const mlist = el(panel, "div", "admin-list"); - const myEmail = (serverConfig.me && serverConfig.me.email) || ""; - for (const m of org.members) { - const row = el(mlist, "div", "admin-item"); - const isSelf = myEmail && m.email.toLowerCase() === myEmail.toLowerCase(); - el(row, "span", "ai-main", m.email + (isSelf ? " (you)" : "")); - if (owner && !isSelf) { - const sel = document.createElement("select"); - for (const r of ["owner", "member"]) { - const o = document.createElement("option"); o.value = r; o.textContent = r; - if (m.role === r) o.selected = true; sel.appendChild(o); - } - sel.onchange = async () => { - try { await api("PATCH", "/api/orgs/" + org.id + "/members/" + encodeURIComponent(m.email), { role: sel.value }); toast("Role updated."); await loadOrgs(); showOrgAdmin(currentOrg()); } - catch (e) { toast(e.message, true); showOrgAdmin(currentOrg()); } - }; - row.appendChild(sel); - const rm = el(row, "button", "ai-del", "Remove"); - rm.onclick = async () => { - if (!(await modalConfirm("Remove member", "Remove " + m.email + " from " + org.name + "?", "Remove", true))) return; - try { await api("DELETE", "/api/orgs/" + org.id + "/members/" + encodeURIComponent(m.email)); toast("Removed."); await loadOrgs(); showOrgAdmin(currentOrg()); } - catch (e) { toast(e.message, true); } - }; - } else { - el(row, "span", "ai-tag", m.role); - } - } - - if (!owner) return; - - // Projects in this org (rename / delete) - el(panel, "h3", null, "Projects"); - const plist = el(panel, "div", "admin-list"); - const orgProjects = projects.filter((p) => p.org === org.id); - if (!orgProjects.length) el(plist, "div", "admin-empty", "No projects yet."); - for (const p of orgProjects) { - const row = el(plist, "div", "admin-item"); - el(row, "span", "ai-main", p.name); - const rn = el(row, "button", "ai-btn", "Rename"); - rn.onclick = async () => { - const name = await modalPrompt("Rename project", "New name", p.name, "Rename"); - if (!name || name === p.name) return; - try { await api("PATCH", "/api/projects/" + p.id, { name }); toast("Renamed."); await loadProjects(); showOrgAdmin(currentOrg()); } - catch (e) { toast(e.message, true); } - }; - const del = el(row, "button", "ai-del", "Delete"); - del.onclick = async () => { - if (!(await modalConfirm("Delete project", "Delete “" + p.name + "”? Its files stay in storage, but it's removed from the hub.", "Delete", true))) return; - try { - await api("DELETE", "/api/projects/" + p.id); - toast("Deleted “" + p.name + "”."); - if (currentProject && currentProject.id === p.id) currentProject = null; - await loadProjects(); - const next = currentOrg(); - if (next) showOrgAdmin(next); else showEmptyState(); - } catch (e) { toast(e.message, true); } - }; - } - - // Invites - const ih = el(panel, "div", "admin-h"); - el(ih, "h3", null, "Invite links"); - const mk = el(ih, "button", "pbtn", "New invite"); - mk.onclick = async () => { - try { - const out = await postJSON("/api/orgs/" + org.id + "/invites"); - const ok = await copyText(out.url); - toast(ok ? "Invite link copied to clipboard." : "Invite created — copy it from the list below."); - showOrgAdmin(currentOrg()); - } catch (e) { toast(e.message, true); } - }; - const ilist = el(panel, "div", "admin-list"); - try { - const invs = (await getJSON("/api/orgs/" + org.id + "/invites")).invites || []; - if (!invs.length) el(ilist, "div", "admin-empty", "No active invite links."); - for (const inv of invs) { - const row = el(ilist, "div", "admin-item"); - const main = el(row, "span", "ai-main mono", inv.url); - main.style.cursor = "pointer"; - main.title = "Copy"; - main.onclick = () => copyText(inv.url).then((ok) => toast(ok ? "Copied." : "Select and copy the link.")); - const meta = (inv.creator ? "by " + inv.creator + " · " : "") + - (inv.uses ? inv.uses + " joined · " : "unused · ") + - "expires " + new Date(inv.expires).toLocaleDateString(); - el(row, "span", "ai-tag", meta); - const rv = el(row, "button", "ai-del", "Revoke"); - rv.onclick = async () => { - if (!(await modalConfirm("Revoke invite", "Revoke this invite link? Anyone still holding it won't be able to join.", "Revoke", true))) return; - try { await api("DELETE", "/api/orgs/" + org.id + "/invites/" + inv.token); toast("Revoked."); showOrgAdmin(currentOrg()); } - catch (e) { toast(e.message, true); } - }; - } - } catch { /* ignore */ } - - // Org-wide share audit - el(panel, "h3", null, "Public share links"); - const slist = el(panel, "div", "admin-list"); - try { - const shs = (await getJSON("/api/orgs/" + org.id + "/shares")).shares || []; - if (!shs.length) el(slist, "div", "admin-empty", "No public shares."); - for (const sh of shs) { - const row = el(slist, "div", "admin-item"); - const main = el(row, "span", "ai-main mono", sh.path); - main.title = sh.url; main.style.cursor = "pointer"; - main.onclick = () => window.open(sh.url, "_blank"); - const meta = (sh.project_name || "") + - (sh.creator ? " · by " + sh.creator : "") + - (sh.created ? " · " + new Date(sh.created).toLocaleDateString() : ""); - el(row, "span", "ai-tag", meta); - const rv = el(row, "button", "ai-del", "Revoke"); - rv.onclick = async () => { - if (!(await modalConfirm("Revoke share link", "Revoke the public link to “" + sh.path + "”? Anyone with the URL will lose access.", "Revoke", true))) return; - try { await api("DELETE", "/api/shares/" + sh.token); toast("Share revoked."); showOrgAdmin(currentOrg()); } - catch (e) { toast(e.message, true); } - }; - } - } catch { /* ignore */ } -} - -/* small DOM helper */ -function el(parent, tag, cls, text) { - const n = document.createElement(tag); - if (cls) n.className = cls; - if (text != null) n.textContent = text; - parent.appendChild(n); - return n; -} - -/* fetch wrapper for methods without a body-returning helper */ -async function api(method, url, body) { - const opt = { method }; - if (body !== undefined) { opt.headers = { "Content-Type": "application/json" }; opt.body = JSON.stringify(body); } - const r = await fetch(url, opt); - if (!r.ok) throw new Error(await r.text()); - return r.status === 204 ? {} : r.json(); -} - -/* In-app modal prompt (text input) — replaces native prompt(). Resolves to - the trimmed string, or null if cancelled. */ -function modalPrompt(title, label, value, okLabel) { - return new Promise((resolve) => { - const back = document.createElement("div"); - back.className = "modal-back"; - back.innerHTML = ``; - back.querySelector("h3").textContent = title; - back.querySelector(".modal-label").textContent = label || ""; - const input = back.querySelector(".modal-input"); - input.value = value || ""; - back.querySelector('[data-a="ok"]').textContent = okLabel || "OK"; - const done = (v) => { back.remove(); document.removeEventListener("keydown", onKey); resolve(v); }; - const onKey = (e) => { if (e.key === "Escape") done(null); if (e.key === "Enter") done(input.value.trim() || null); }; - back.querySelector('[data-a="cancel"]').onclick = () => done(null); - back.querySelector('[data-a="ok"]').onclick = () => done(input.value.trim() || null); - back.onclick = (e) => { if (e.target === back) done(null); }; - document.addEventListener("keydown", onKey); - document.body.appendChild(back); - input.focus(); - input.select(); - }); -} - -/* In-app confirm — replaces native confirm(). `danger` styles the confirm - button as destructive. Resolves true/false. */ -function modalConfirm(title, message, confirmLabel, danger) { - return new Promise((resolve) => { - const back = document.createElement("div"); - back.className = "modal-back"; - back.innerHTML = ``; - back.querySelector("h3").textContent = title; - back.querySelector(".modal-msg").textContent = message || ""; - back.querySelector('[data-a="ok"]').textContent = confirmLabel || "Confirm"; - const done = (v) => { back.remove(); document.removeEventListener("keydown", onKey); resolve(v); }; - const onKey = (e) => { if (e.key === "Escape") done(false); if (e.key === "Enter") done(true); }; - back.querySelector('[data-a="cancel"]').onclick = () => done(false); - back.querySelector('[data-a="ok"]').onclick = () => done(true); - back.onclick = (e) => { if (e.target === back) done(false); }; - document.addEventListener("keydown", onKey); - document.body.appendChild(back); - back.querySelector('[data-a="ok"]').focus(); - }); -} - -/* clipboard copy that never throws on a non-HTTPS origin (where - navigator.clipboard is undefined). Returns true on success. */ -async function copyText(text) { - try { - if (navigator.clipboard) { await navigator.clipboard.writeText(text); return true; } - } catch { /* fall through */ } - return false; -} - -/* transient toast, replacing blocking alert() */ -let toastTimer = null; -function toast(msg, isErr) { - let t = $("toast"); - if (!t) { t = document.createElement("div"); t.id = "toast"; document.body.appendChild(t); } - t.textContent = msg; - t.className = "show" + (isErr ? " err" : ""); - clearTimeout(toastTimer); - toastTimer = setTimeout(() => { t.className = ""; }, 3200); -} - -/* Admin approval bar: a hub admin sees pending signups to approve/deny. */ -async function updateAdminBar() { - const bar = $("adminbar"); - if (!bar) return; - if (!(serverConfig.auth && serverConfig.auth.admin)) { bar.hidden = true; return; } - let pending = []; - try { pending = (await getJSON("/api/admin/pending")).pending || []; } catch { } - bar.hidden = false; - bar.innerHTML = svgIcon("shield") + `Admin${pending.length ? " · " + pending.length : ""}`; - bar.title = "Hub administration — signup policy" + (pending.length ? " and pending approvals" : ""); - bar.onclick = () => showHubSettings(); -} - -/* Hub-admin settings: signup/access policy. Verification & approval are - live toggles; the domain allowlist and admin list are shown read-only - (they're server-config owned, deliberately not browser-editable). */ -async function showHubSettings() { - let pol = {}; - try { pol = await getJSON("/api/admin/policy"); } catch (e) { toast(e.message, true); return; } - currentPath = null; markActive(); closeSidebarOnMobile(); - $("crumb").textContent = "Signup & access"; - $("share-btn").hidden = $("history-btn").hidden = $("download").hidden = $("more-btn").hidden = true; - const box = $("content"); - box.className = "view"; - box.innerHTML = `

Signup & access

-

Who can create an account on this hub, and how new accounts are vetted.

`; - const panel = box.querySelector(".admin"); - - el(panel, "h3", null, "New-account vetting"); - const toggles = el(panel, "div", "admin-list"); - const mkToggle = (label, desc, key, on, disabled) => { - const row = el(toggles, "label", "admin-item toggle"); - const left = el(row, "span", "ai-main"); - el(left, "div", "tg-label", label); - el(left, "div", "tg-desc", desc); - const cb = document.createElement("input"); - cb.type = "checkbox"; cb.checked = !!on; cb.dataset.key = key; - if (disabled) { cb.disabled = true; row.style.opacity = ".55"; } - row.appendChild(cb); - return cb; - }; - const ver = mkToggle("Require email verification", - pol.mailer ? "New accounts must click an emailed link before they can sign in — proves they control the address." - : "Configure SMTP on the server (auth.smtp) to enable email verification.", - "require_verification", pol.require_verification && pol.mailer, !pol.mailer); - const app = mkToggle("Require admin approval", - "New accounts wait for a hub admin to approve them before they gain access.", - "require_approval", pol.require_approval); - const save = el(panel, "button", "pbtn", "Save policy"); - save.style.marginTop = "14px"; - save.onclick = async () => { - try { - await postJSON("/api/admin/policy", { require_verification: ver.checked, require_approval: app.checked }); - toast("Signup policy saved."); - } catch (e) { toast(e.message, true); } - }; - - el(panel, "h3", null, "Who can sign up"); - const info = el(panel, "div", "admin-list"); - const dom = el(info, "div", "admin-item"); - el(dom, "span", "ai-main", "Allowed email domains"); - el(dom, "span", "ai-tag", (pol.allowed_domains && pol.allowed_domains.length) ? pol.allowed_domains.map((d) => "@" + d).join(", ") : "any"); - const sg = el(info, "div", "admin-item"); - el(sg, "span", "ai-main", "Self-signup"); - el(sg, "span", "ai-tag", pol.allow_signup ? "open" : "invite-only"); - const ad = el(info, "div", "admin-item"); - el(ad, "span", "ai-main", "Hub admins"); - el(ad, "span", "ai-tag", (pol.admins && pol.admins.length) ? pol.admins.join(", ") : "none"); - el(panel, "p", "admin-sub", "Domains and admins are set in the server config file (they can't be widened from the browser)."); - - // Pending approvals live here too, so this is the single admin home. - el(panel, "h3", null, "Pending signups"); - const plist = el(panel, "div", "admin-list"); - let pending = []; - try { pending = (await getJSON("/api/admin/pending")).pending || []; } catch { } - if (!pending.length) el(plist, "div", "admin-empty", "No one is waiting for approval."); - for (const u of pending) { - const row = el(plist, "div", "admin-item"); - el(row, "span", "ai-main", (u.name ? u.name + " · " : "") + u.email); - const ok = el(row, "button", "pbtn", "Approve"); - ok.onclick = async () => { try { await postJSON("/api/admin/pending/" + u.id + "/approve"); toast("Approved " + u.email); updateAdminBar(); showHubSettings(); } catch (e) { toast(e.message, true); } }; - const no = el(row, "button", "ai-del", "Deny"); - no.onclick = async () => { try { await postJSON("/api/admin/pending/" + u.id + "/deny"); toast("Denied " + u.email); updateAdminBar(); showHubSettings(); } catch (e) { toast(e.message, true); } }; - } -} - -async function showPending() { - let pending = []; - try { pending = (await getJSON("/api/admin/pending")).pending || []; } catch { } - currentPath = null; markActive(); - $("crumb").textContent = "Pending signups"; - $("share-btn").hidden = $("history-btn").hidden = $("download").hidden = $("more-btn").hidden = true; - const box = $("content"); - box.className = "view"; - box.innerHTML = `

Pending signups

`; - const panel = box.querySelector(".admin"); - const list = el(panel, "div", "admin-list"); - if (!pending.length) el(list, "div", "admin-empty", "No one is waiting for approval."); - for (const u of pending) { - const row = el(list, "div", "admin-item"); - el(row, "span", "ai-main", (u.name ? u.name + " · " : "") + u.email); - const ok = el(row, "button", "pbtn", "Approve"); - ok.onclick = async () => { try { await postJSON("/api/admin/pending/" + u.id + "/approve"); toast("Approved " + u.email); updateAdminBar(); showPending(); } catch (e) { toast(e.message, true); } }; - const no = el(row, "button", "ai-del", "Deny"); - no.onclick = async () => { try { await postJSON("/api/admin/pending/" + u.id + "/deny"); toast("Denied " + u.email); updateAdminBar(); showPending(); } catch (e) { toast(e.message, true); } }; - } -} - -function refreshAll() { - loadProjects(); - updateOrgBar(); - if (currentProject) refreshTree(); -} - -/* Native path routing (no hash, no %2F): - volume mode: / - hub mode: // - invite: /join/ - Each path segment is percent-encoded for odd characters, but the "/" - separators stay literal so the URL reads like a real file path. */ -function encodePath(p) { return p.split("/").map(encodeURIComponent).join("/"); } -function decodePath(p) { return p.split("/").map(decodeURIComponent).join("/"); } - -/* Special views are RESTful routes under the project — the first segment - after the project id is reserved when it names a view: - //insights the Insights dashboard - //history[/] change feed (whole project / subtree / file) - (Root-level files literally named "insights" or "history" lose the URL - shortcut and remain reachable through the tree.) */ -const VIEW_ROUTES = new Set(["insights", "history"]); - -function parseRoute() { - const raw = location.pathname.replace(/^\/+/, ""); - if (serverConfig.mode !== "hub") return { path: raw ? decodePath(raw) : "" }; - const slash = raw.indexOf("/"); - if (slash === -1) return { project: raw, path: "" }; - const r = { project: raw.slice(0, slash), path: decodePath(raw.slice(slash + 1)) }; - const seg = r.path.indexOf("/"); - const head = seg === -1 ? r.path : r.path.slice(0, seg); - if (VIEW_ROUTES.has(head)) { - r.view = head; - r.viewTarget = seg === -1 ? "" : r.path.slice(seg + 1).replace(/\/+$/, ""); - r.path = ""; - } - return r; -} - -/* The URL for a file within the current context. */ -function urlForPath(path) { - const enc = encodePath(path); - if (serverConfig.mode === "hub" && currentProject) { - return "/" + currentProject.id + (enc ? "/" + enc : ""); - } - return "/" + enc; -} - -/* The URL for a special view of the current project. */ -function urlForView(view, q) { - let s = (serverConfig.mode === "hub" && currentProject ? "/" + currentProject.id : "") + "/" + view; - if (view === "history" && q) { - const target = "path" in q ? q.path : (q.prefix || "").replace(/\/+$/, ""); - if (target) s += "/" + encodePath(target); - } - return s; -} - -/* Dispatch a parsed route to the right view (after the tree has loaded). */ -function applyRoute(r) { - if (r.view === "insights") return showInsights(false); - if (r.view === "history") { - // The route stores one target; the tree says whether it is a folder - // (subtree feed) or a file (version list). - const t = r.viewTarget || ""; - const hq = !t ? { prefix: "" } : dirIndex.has(t) ? { prefix: t + "/" } : { path: t }; - return showHistory(hq, false); - } - if (r.path) return openPath(r.path); - if (serverConfig.mode === "hub" && currentProject) showProjectHome("replace"); -} - -/* Per-route scroll positions of the content pane, so back/forward returns - to where the user was (e.g. deep in a folder's change feed). */ -const scrollMemo = new Map(); -let routeKey = location.pathname + location.search; -let pendingScroll = null; // set by popstate, consumed by the next render -function rememberScroll() { scrollMemo.set(routeKey, $("content").scrollTop); } -function applyPendingScroll(final) { - if (pendingScroll == null) return; - $("content").scrollTo({ top: pendingScroll, behavior: "instant" }); - if (final) pendingScroll = null; -} - -/* Push a route without reloading, skipping a no-op that would just stack a - duplicate history entry (e.g. when boot opens the file already in the URL). */ -function pushURL(url) { - if (location.pathname + location.search === url) return; - rememberScroll(); - history.pushState(null, "", url); - routeKey = url; -} -/* Replace the current entry (landing views that shouldn't stack). */ -function replaceURL(url) { - history.replaceState(null, "", url); - routeKey = url; -} -function syncURL(path) { pushURL(urlForPath(path)); } - -/* ---- tree ---- */ -let lastTreeJSON = ""; // skip re-rendering when nothing changed (no flicker) -async function refreshTree() { - if (serverConfig.mode === "hub" && !currentProject) return; - let root; - try { - root = await getJSON(apiBase + "tree"); - } catch { return; } // keep the last good tree - // The 15s poll usually returns the same tree — rebuilding the DOM anyway - // makes the sidebar (and any open folder listing) visibly flicker. - const json = JSON.stringify(root); - if (json === lastTreeJSON) { - refreshHeat(); - return; - } - lastTreeJSON = json; - flatFiles = []; - dirIndex = new Map(); - const kids = root.children || []; - // First render of a project's tree: every folder starts closed, except a - // lone root folder — opening it spares the user a single shut folder. - if (treeFirstLoad) { - treeFirstLoad = false; - const rootDirs = kids.filter((c) => c.dir); - if (rootDirs.length === 1) expanded.add(rootDirs[0].path); - } - const nav = $("tree"); - nav.innerHTML = ""; - nav.appendChild(renderChildren(kids)); - markActive(); - // A folder listing shows live tree data — keep it in step with the tree. - if (currentPath && dirIndex.has(currentPath) && $("content").querySelector(".dirlist")) { - renderFolderListing(currentPath); - } - refreshHeat(); -} - -/* ---- read heat ---- - 30-day read counts per path from the heat API (hub only). Counts only — - the server never says who read what. Fetched lazily alongside the tree. */ -let lastHeatJSON = ""; -async function refreshHeat(force) { - if (!(serverConfig.mode === "hub" && currentProject)) return; - if (!(serverConfig.reads && serverConfig.reads.enabled)) return; - if (!force && Date.now() - heatAt < 60000) return; - heatAt = Date.now(); // set before the fetch so failures don't hammer - let out; - try { - out = await getJSON(apiBase + "heat?days=30"); - } catch { return; } // keep the last good heat - const json = JSON.stringify(out.entries || {}); - if (json === lastHeatJSON) return; // unchanged: no re-render, no flicker - lastHeatJSON = json; - heatMap = out.entries || {}; - if (currentPath && dirIndex.has(currentPath) && $("content").querySelector(".dirlist")) { - renderFolderListing(currentPath); - } -} - -/* Heat for one listing entry: a file's own bucket, or the subtree sum for a - folder. Null when there is nothing to show. */ -function heatFor(path, isDir) { - if (!heatMap) return null; - if (!isDir) return heatMap[path] || null; - const agg = { human: 0, agent: 0, share: 0 }; - for (const [p, e] of Object.entries(heatMap)) { - if (!p.startsWith(path + "/")) continue; - agg.human += e.human || 0; - agg.agent += e.agent || 0; - agg.share += e.share || 0; - } - return agg.human || agg.agent || agg.share ? agg : null; -} - -function heatTotal(e) { return (e.human || 0) + (e.agent || 0) + (e.share || 0); } - -function heatText(e) { - const total = heatTotal(e); - if (!total) return ""; - let s = total + (total === 1 ? " read" : " reads"); - if (e.agent) s += " (" + e.agent + " agent)"; - return s; -} - -/* Dot intensity 1–4, log-ish steps: 1–2, 3–9, 10–29, 30+ reads. */ -function heatLevel(e) { - const total = heatTotal(e); - if (!total) return 0; - if (total < 3) return 1; - if (total < 10) return 2; - if (total < 30) return 3; - return 4; -} - -function renderChildren(children) { - const ul = document.createElement("ul"); - for (const n of children) ul.appendChild(renderNode(n)); - return ul; -} - -function renderNode(n) { - const li = document.createElement("li"); - li.className = n.dir ? "dir" : "file"; - const row = document.createElement("div"); - row.className = "row"; - row.dataset.path = n.path; - const chev = document.createElement("span"); - chev.className = "chev"; - chev.innerHTML = svgIcon("chevd"); - const ticon = document.createElement("span"); - ticon.className = "ticon"; - ticon.innerHTML = svgIcon(n.dir ? "folder" : "doc"); - const label = document.createElement("span"); - label.className = "label"; - label.textContent = n.name; - row.append(chev, ticon, label); - // Keyboard-operable: the row behaves as a button. - row.tabIndex = 0; - row.setAttribute("role", "button"); - row.title = n.name; - row.onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); row.click(); } }; - li.appendChild(row); - if (n.dir) { - dirIndex.set(n.path, n); - li.appendChild(renderChildren(n.children || [])); - const open = expanded.has(n.path); - if (!open) li.classList.add("collapsed"); - row.setAttribute("aria-expanded", String(open)); - const toggle = () => { - li.classList.toggle("collapsed"); - const isCollapsed = li.classList.contains("collapsed"); - isCollapsed ? expanded.delete(n.path) : expanded.add(n.path); - row.setAttribute("aria-expanded", String(!isCollapsed)); - }; - // The chevron only folds; the row selects the folder (opens it in the - // tree and lists its contents in the main pane). Clicking the folder - // whose listing is already showing folds/unfolds it, like a plain tree; - // from any other view (file, history) it brings the listing back. - chev.onclick = (e) => { e.stopPropagation(); toggle(); }; - row.onclick = () => { - if (currentPath === n.path && $("content").querySelector(".dirlist")) { toggle(); return; } - openFolder(n.path); - }; - } else { - flatFiles.push({ path: n.path, name: n.name, time: n.time }); - row.onclick = () => openFile(n.path); - } - return li; -} - -function markActive() { - for (const el of document.querySelectorAll("#tree .row.active")) el.classList.remove("active"); - if (!currentPath) return; - const row = document.querySelector(`#tree .row[data-path="${CSS.escape(currentPath)}"]`); - if (row) row.classList.add("active"); -} - -/* Mark every ancestor folder of a path as open. */ -function expandTo(filePath) { - const parts = filePath.split("/"); - let acc = ""; - for (let i = 0; i < parts.length - 1; i++) { - acc = acc ? acc + "/" + parts[i] : parts[i]; - expanded.add(acc); - } -} - -/* Reflect the `expanded` set onto the already-rendered tree DOM. */ -function applyTreeExpansion() { - for (const li of document.querySelectorAll("#tree li.dir")) { - const row = li.querySelector(":scope > .row"); - const open = row && expanded.has(row.dataset.path); - li.classList.toggle("collapsed", !open); - if (row) row.setAttribute("aria-expanded", String(!!open)); - } -} - -/* Opening a file (search, wikilink, deep link) unfolds the path to it and - scrolls the row into view so the selection is never hidden in a shut folder. */ -function revealInTree(p) { - expandTo(p); - applyTreeExpansion(); - const row = document.querySelector(`#tree .row[data-path="${CSS.escape(p)}"]`); - if (row) row.scrollIntoView({ block: "center" }); -} - -/* ---- breadcrumb ---- - Every ancestor segment is a link to that folder's listing; the last - segment is the current page. */ -function setCrumb(p) { - const c = $("crumb"); - c.innerHTML = ""; - const parts = p.split("/"); - let acc = ""; - parts.forEach((seg, i) => { - acc = acc ? acc + "/" + seg : seg; - if (i) el(c, "span", "crumb-sep", "/"); - const last = i === parts.length - 1; - const s = el(c, "span", last ? null : "crumb-seg", seg); - if (!last) { - const target = acc; - s.title = target; - s.onclick = () => { if (dirIndex.has(target)) openFolder(target); }; - } - }); -} - -/* ---- folder pane ---- */ - -/* Open a path of either kind — a route or link doesn't know which it is - until the tree has loaded. */ -function openPath(p) { - if (dirIndex.has(p)) openFolder(p); - else openFile(p); -} - -/* Selecting a folder: unfold it in the tree, highlight it, and list what's - inside it in the main pane. */ -function openFolder(p) { - if (!dirIndex.has(p)) return; - currentPath = p; - syncURL(p); - expandTo(p); - expanded.add(p); // the selected folder itself opens, not just its ancestors - applyTreeExpansion(); - markActive(); - const row = document.querySelector(`#tree .row[data-path="${CSS.escape(p)}"]`); - if (row) row.scrollIntoView({ block: "nearest" }); - closeSidebarOnMobile(); - setCrumb(p); - $("meta").textContent = ""; - $("download").hidden = true; - $("more-btn").hidden = !(serverConfig.mode === "hub" && currentProject); - updateShareButton(); - if (pendingScroll == null) $("content").scrollTop = 0; // fresh navigation starts at the top - renderFolderListing(p); -} - -function renderFolderListing(p) { - const node = dirIndex.get(p); - if (!node) return; - const content = $("content"); - // Scroll intent: a back/forward restore wins; an in-place refresh (tree or - // heat poll re-render) keeps the reader where they are; openFolder resets - // to the top before calling us. - const keepTop = pendingScroll != null ? pendingScroll : content.scrollTop; - pendingScroll = null; - content.className = "view"; - content.innerHTML = ""; - const wrap = el(content, "div", "dirlist"); - const head = el(wrap, "h1", "dl-title"); - const hicon = el(head, "span", "dl-title-icon"); - hicon.innerHTML = svgIcon("folder"); - el(head, "span", null, node.name); - const kids = (node.children || []).slice() - .sort((a, b) => (b.dir - a.dir) || a.name.localeCompare(b.name)); - const dirs = kids.filter((c) => c.dir).length; - const files = kids.length - dirs; - const counts = []; - if (dirs) counts.push(dirs + (dirs === 1 ? " folder" : " folders")); - if (files) counts.push(files + (files === 1 ? " file" : " files")); - const folderHeat = heatFor(p, true); - if (folderHeat) counts.push(heatText(folderHeat) + " in 30 days"); - el(wrap, "p", "dl-sub", counts.join(" · ") || "Empty folder"); - if (!kids.length) { - el(wrap, "div", "dl-empty", "Nothing in this folder yet."); - if (keepTop) content.scrollTo({ top: keepTop, behavior: "instant" }); - renderFolderHistory(wrap, p, keepTop); - return; - } - const list = el(wrap, "div", "dl-items"); - for (const c of kids) { - const row = el(list, "div", "dl-row"); - row.tabIndex = 0; - row.setAttribute("role", "button"); - row.title = c.path; - const icon = el(row, "span", "ticon"); - icon.innerHTML = svgIcon(c.dir ? "folder" : "doc"); - el(row, "span", "dl-name", c.name); - let meta = ""; - if (c.dir) { - const n = (c.children || []).length; - meta = n + (n === 1 ? " item" : " items"); - } else { - meta = [c.size ? humanSize(c.size) : "", c.time ? new Date(c.time).toLocaleDateString() : ""] - .filter(Boolean).join(" · "); - } - const he = heatFor(c.path, !!c.dir); - if (he) { - const dot = el(row, "span", "heatdot lvl" + heatLevel(he)); - dot.title = heatText(he) + " in 30 days"; - meta = heatText(he) + (meta ? " · " + meta : ""); - } - el(row, "span", "dl-meta", meta); - row.onclick = () => openPath(c.path); - row.onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); row.click(); } }; - } - if (keepTop) content.scrollTo({ top: keepTop, behavior: "instant" }); - renderFolderHistory(wrap, p, keepTop); -} - -/* The folder's change feed, straight from the journals: files added, edited, - and deleted anywhere under it, newest first. Hub-only — a plain-folder - viewer has no journals to read. */ -function renderFolderHistory(wrap, p, restoreTop) { - if (!(serverConfig.mode === "hub" && currentProject)) return; - const sec = el(wrap, "div", "dl-history"); - getJSON(apiBase + "history?prefix=" + encodeURIComponent(p + "/") + "&n=20").then((out) => { - const entries = out.entries || []; - if (!entries.length) return sec.remove(); - el(sec, "h3", "dl-h3", "Recent changes"); - const list = el(sec, "div", "history dl-hlist"); - for (const e of entries) list.appendChild(historyEntryRow(e)); - const more = el(sec, "button", "ai-btn dl-more", "Full history"); - more.onclick = () => showHistory({ prefix: p + "/" }); - // The feed adds height after the listing rendered; a restored scroll - // position (back/forward) may only fit now. - if (restoreTop) $("content").scrollTo({ top: restoreTop, behavior: "instant" }); - }).catch(() => sec.remove()); -} - -/* ---- file pane ---- */ -async function openFile(p) { - const restoreTop = pendingScroll != null ? pendingScroll : 0; - pendingScroll = null; - currentPath = p; - syncURL(p); - markActive(); - revealInTree(p); - closeSidebarOnMobile(); - setCrumb(p); - updateShareButton(); - const dl = $("download"); - dl.href = apiBase + "download?path=" + encodeURIComponent(p); - dl.hidden = false; - $("more-btn").hidden = false; - const content = $("content"); - content.className = "markdown"; // document view: markdown type rules apply - content.innerHTML = ""; - $("meta").textContent = ""; - try { - if (MD_EXT.test(p)) { - const doc = await getJSON(apiBase + "render?path=" + encodeURIComponent(p)); - content.innerHTML = doc.html; - fixLinks(content, p); - showMeta(doc); - } else if (IMG_EXT.test(p)) { - const img = document.createElement("img"); - img.src = fileURL(p); - img.alt = p; - content.appendChild(img); - } else if (TEXT_EXT.test(p)) { - const r = await fetch(fileURL(p)); - if (!r.ok) throw new Error(await r.text()); - const pre = document.createElement("pre"); - pre.className = "plain"; - pre.textContent = await r.text(); - content.appendChild(pre); - } else { - content.innerHTML = - `
` + - `

No preview for this file type.

` + - `Download
`; - content.querySelector(".name").textContent = p.split("/").pop(); - } - } catch (err) { - content.innerHTML = `
`; - content.querySelector(".empty").textContent = "Could not load file: " + err.message; - } - content.scrollTo({ top: restoreTop, behavior: "instant" }); -} - -function showMeta(doc) { - const parts = []; - if (doc.author) parts.push(doc.author + (doc.device ? " on " + doc.device : "")); - if (doc.time) parts.push(new Date(doc.time).toLocaleString()); - const he = heatMap && heatMap[doc.path]; - if (he && heatTotal(he)) parts.push(heatText(he) + " / 30d"); - $("meta").textContent = parts.join(" · "); -} - -/* Rewrite rendered-markdown links: wiki: targets resolve by basename, and - relative links/images resolve against the current file's folder. */ -function fixLinks(scope, p) { - const dir = p.includes("/") ? p.slice(0, p.lastIndexOf("/")) : ""; - for (const img of scope.querySelectorAll("img")) { - const src = img.getAttribute("src") || ""; - if (!/^([a-z]+:|\/)/i.test(src)) img.src = fileURL(join(dir, src)); - } - for (const a of scope.querySelectorAll("a")) { - const href = a.getAttribute("href") || ""; - if (href.startsWith("wiki:")) { - const target = decodeURIComponent(href.slice(5)); - a.onclick = (e) => { e.preventDefault(); openWikilink(target); }; - } else if (!/^([a-z]+:|\/|#)/i.test(href)) { - const target = join(dir, decodeURIComponent(href)); - a.onclick = (e) => { e.preventDefault(); openFile(target); }; - } else if (/^https?:/i.test(href)) { - a.target = "_blank"; - a.rel = "noopener"; - } - } -} - -function openWikilink(target) { - const want = target.toLowerCase(); - const hit = - flatFiles.find((f) => f.path.toLowerCase() === want || f.path.toLowerCase() === want + ".md") || - flatFiles.find((f) => { - const n = f.name.toLowerCase(); - return n === want || n === want + ".md"; - }); - if (hit) openFile(hit.path); -} - -/* A clear, explicitly-public share confirmation: warns that anyone with the - link can view, and offers copy / open / revoke. */ -function showShareDialog(url, copied) { - const back = document.createElement("div"); - back.className = "modal-back"; - back.innerHTML = ` - `; - back.querySelector(".modal-url").textContent = url; - const close = () => back.remove(); - back.onclick = (e) => { if (e.target === back) close(); }; - const token = url.split("/s/")[1]; - back.querySelector('[data-a="copy"]').onclick = () => copyText(url).then((ok) => toast(ok ? "Copied." : "Select and copy the link above.")); - back.querySelector('[data-a="open"]').onclick = () => window.open(url, "_blank"); - back.querySelector('[data-a="close"]').onclick = close; - back.querySelector('[data-a="revoke"]').onclick = async () => { - try { await api("DELETE", "/api/shares/" + token); toast("Link revoked — it no longer works."); close(); } - catch (e) { toast(e.message, true); } - }; - document.body.appendChild(back); -} - -function join(dir, rel) { - const parts = (dir ? dir.split("/") : []).concat(rel.split("/")); - const out = []; - for (const s of parts) { - if (s === "" || s === ".") continue; - if (s === "..") out.pop(); - else out.push(s); - } - return out.join("/"); -} - -/* ---- share ---- - Mint a public URL for the open file: anyone with the link can view it - (rendered, sandboxed), no account needed. Always the latest content. */ -function updateShareButton() { - const btn = $("share-btn"); - // Shares are per-file; a selected folder has nothing to mint. - btn.hidden = !(serverConfig.mode === "hub" && currentProject && currentPath && !dirIndex.has(currentPath)); - btn.onclick = async () => { - try { - const r = await fetch(apiBase + "shares", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ path: currentPath }), - }); - if (!r.ok) throw new Error(await r.text()); - const share = await r.json(); - const copied = await copyText(share.url); - showShareDialog(share.url, copied); - } catch (err) { - toast("Share failed: " + err.message, true); - } - }; -} - -/* ---- project home ---- - The index page of a project (/): how to mount the project as a - local folder and connect a coding agent, with the Insights dashboard - embedded below for admins/org-owners. The guide shows real, copyable - commands with this hub's URL and this project's id filled in. */ - -const GUIDE_AGENTS = [ - { key: "claude", label: "Claude Code & Cowork" }, - { key: "hermes", label: "Hermes", hook: "hermes", - note: "Registers BearDrive's hooks in Hermes's config: pull before every turn, push after edits " + - "with a session note, and report file reads to Insights." }, - { key: "codex", label: "Codex", hook: "codex", - note: "Registers hooks in .codex/hooks.json.", - extra: "Run /hooks inside Codex once to trust the project's .codex layer — after that every turn " + - "pulls, edits push automatically, and reads are reported to Insights." }, -]; - -let guideAgent = "claude"; -try { guideAgent = localStorage.getItem("bdrive-guide-agent") || "claude"; } catch { /* private mode */ } - -function guideSteps(agent) { - const origin = location.origin, pid = currentProject.id; - - // Claude Code and Claude Cowork share one plugin: install it once, then - // /beardrive:install does the whole setup conversationally. - if (agent.key === "claude") { - return [ - { title: "Add the BearDrive plugin", - desc: "One time, in any Claude Code session. The plugin ships the beardrive skill, the /beardrive " + - "commands, and turn-boundary sync hooks — and Claude Cowork shares the same plugins, so " + - "installing it once covers both.", - code: "/plugin marketplace add runbear-io/beardrive\n/plugin install beardrive@beardrive" }, - { title: "Set up this project conversationally", - desc: "In a Claude Code or Cowork session in the folder where you want the files, run:", - code: "/beardrive:install connect to " + origin + ", project " + pid, - extra: "Claude installs the CLI, signs this machine in, mounts the project, and registers the " + - "sync hooks — pull the latest before every turn, push after edits (stamped with the session " + - "that made them), and report file reads to Insights. It asks before anything it changes." }, - ]; - } - - const slug = (currentProject.name || "project").toLowerCase().replace(/[^a-z0-9._-]+/g, "-") || "project"; - return [ - { title: "Install the BearDrive CLI", - desc: "One static binary. Homebrew on macOS and Linux; releases and `go install` also work.", - code: "brew install runbear-io/tap/beardrive" }, - { title: "Sign in to this hub", - desc: "Opens the browser once and stores a device token on this machine — the synced folder itself never holds credentials.", - code: "bdrive login " + origin }, - { title: "Mount the project into a local folder", - desc: "Run it where you want the files. An existing folder works too — contents merge, and re-running init later (or after moving the folder) just resumes.", - code: "mkdir -p ~/" + slug + " && cd ~/" + slug + "\nbdrive init --project " + pid }, - { title: "Connect " + agent.label, - desc: agent.note, - code: "bdrive hooks install --agent " + agent.hook, - extra: agent.extra }, - ]; -} - -function renderConnectGuide(content) { - const wrap = el(content, "div", "guide"); - el(wrap, "h1", "in-title", currentProject.name); - el(wrap, "p", "dl-sub", - "Mount this project as a folder on any machine and connect your coding agent: files sync both " + - "ways in the background, every change is journaled with who made it, and agent reads feed Insights."); - const tabs = el(wrap, "div", "gd-tabs"); - const body = el(wrap, "div", "gd-body"); - const render = () => { - const agent = GUIDE_AGENTS.find((a) => a.key === guideAgent) || GUIDE_AGENTS[0]; - for (const b of tabs.children) b.classList.toggle("active", b.dataset.key === agent.key); - body.innerHTML = ""; - guideSteps(agent).forEach((s, i) => { - const step = el(body, "div", "gd-step"); - const head = el(step, "div", "gd-step-head"); - el(head, "span", "gd-num", String(i + 1)); - el(head, "span", "gd-step-title", s.title); - if (s.desc) el(step, "p", "gd-desc", s.desc); - if (s.code) { - const pre = el(step, "pre", "gd-code"); - el(pre, "code", null, s.code); - const btn = el(pre, "button", "gd-copy", "Copy"); - btn.onclick = async () => { - btn.textContent = (await copyText(s.code)) ? "Copied" : "Copy failed"; - setTimeout(() => { btn.textContent = "Copy"; }, 1400); - }; - } - if (s.extra) el(step, "p", "gd-desc gd-extra", s.extra); - }); - el(body, "p", "gd-done", - "That's it — the folder now syncs on its own. Every agent turn starts from the latest state, " + - "edits appear here (and on every teammate's mount) within seconds, and what your agents read " + - "shows up in Insights."); - }; - for (const a of GUIDE_AGENTS) { - const b = el(tabs, "button", "gd-tab", a.label); - b.dataset.key = a.key; - b.onclick = () => { - guideAgent = a.key; - try { localStorage.setItem("bdrive-guide-agent", a.key); } catch { /* private mode */ } - render(); - }; - } - render(); -} - -async function showProjectHome(push = true) { - if (!(serverConfig.mode === "hub" && currentProject)) return; - if (push === "replace") replaceURL("/" + currentProject.id); - else if (push) pushURL("/" + currentProject.id); - const restoreTop = pendingScroll != null ? pendingScroll : 0; - pendingScroll = null; - currentPath = null; - markActive(); - $("crumb").textContent = currentProject.name; - $("meta").textContent = ""; - $("download").hidden = true; - $("more-btn").hidden = true; - const content = $("content"); - content.className = "view"; - content.innerHTML = ""; - renderConnectGuide(content); - if (canSeeInsights()) { - const host = el(content, "div", "home-insights"); - await refreshHeat(true); - insightsDevices = null; - try { - insightsDevices = (await getJSON(apiBase + "heat?by=device&days=30")).devices || []; - } catch { /* older server: the coverage section simply doesn't render */ } - if (!host.isConnected) return; // user navigated away during the fetches - renderInsights(host, "all"); - } - content.scrollTo({ top: restoreTop, behavior: "instant" }); -} - -/* ---- insights: the read×write matrix ---- - Every file plotted by how much it is read (30 days, from the heat API) - against how long since it last changed (from the tree). The hot-but-stale - quadrant is the danger zone: knowledge people still rely on that nobody - maintains. Admin/org-owner only — members get the ambient heat dots. */ - -const HOT_READS = 3; // ≥ this many reads/30d = hot -const STALE_DAYS = 30; // ≥ this many days since last write = stale - -function canSeeInsights() { - if (!(serverConfig.mode === "hub" && currentProject)) return false; - if (!(serverConfig.reads && serverConfig.reads.enabled)) return false; - if (serverConfig.auth && serverConfig.auth.admin) return true; - const org = currentOrg(); - return !!(org && org.role === "owner"); -} - -let insightsDevices = null; // /heat?by=device breakdown, fetched per Insights open - -async function showInsights(push = true) { - if (!canSeeInsights()) return; - // A real route (?v=insights): refresh, deep links, and back/forward work. - if (push === "replace") replaceURL(urlForView("insights")); - else if (push) pushURL(urlForView("insights")); - const restoreTop = pendingScroll != null ? pendingScroll : 0; - pendingScroll = null; - await refreshHeat(true); - insightsDevices = null; - try { - insightsDevices = (await getJSON(apiBase + "heat?by=device&days=30")).devices || []; - } catch { /* older server: the coverage section simply doesn't render */ } - currentPath = null; - markActive(); - $("crumb").textContent = "Insights — " + currentProject.name; - $("meta").textContent = ""; - $("download").hidden = true; - $("more-btn").hidden = true; - const content = $("content"); - content.className = "view"; - renderInsights(content, "all"); - content.scrollTo({ top: restoreTop, behavior: "instant" }); -} - -/* SVG element helper for the insights charts. */ -function svgEl(parent, tag, attrs, text) { - const n = document.createElementNS("http://www.w3.org/2000/svg", tag); - for (const [k, v] of Object.entries(attrs || {})) n.setAttribute(k, v); - if (text != null) n.textContent = text; - if (parent) parent.appendChild(n); - return n; -} - -/* Staleness color: fresh green → amber → red over 0..300 days. */ -function staleColor(days) { - const stops = [[76, 195, 138], [232, 196, 84], [224, 93, 93]]; - const t = Math.min(1, Math.max(0, days / 300)) * (stops.length - 1); - const i = Math.min(stops.length - 2, Math.floor(t)), f = t - i; - const c = stops[i].map((v, k) => Math.round(v + (stops[i + 1][k] - v) * f)); - return `rgb(${c[0]},${c[1]},${c[2]})`; -} - -function renderInsights(content, lens) { - content.innerHTML = ""; - const wrap = el(content, "div", "insights"); - el(wrap, "h1", "in-title", "Knowledge insights"); - el(wrap, "p", "dl-sub", - "Reads over the last 30 days × how long since each file changed. " + - "Hot but stale knowledge — read a lot, maintained by nobody — is the danger zone."); - const bar = el(wrap, "div", "in-lens"); - for (const l of ["all", "human", "agent"]) { - const label = l === "all" ? "All reads" : l === "human" ? "Human reads" : "Agent reads"; - const b = el(bar, "button", "in-lens-btn" + (l === lens ? " active" : ""), label); - b.onclick = () => renderInsights(content, l); - } - - const readsOf = (e) => (lens === "all" ? heatTotal(e) : e[lens] || 0); - const now = Date.now(); - const pts = flatFiles.map((f) => { - const e = (heatMap && heatMap[f.path]) || {}; - const days = f.time ? Math.max(0, (now - new Date(f.time).getTime()) / 86400000) : 0; - const reads = readsOf(e); - return { path: f.path, reads, agent: e.agent || 0, total: heatTotal(e), days, - danger: reads >= HOT_READS && days >= STALE_DAYS }; - }); - - el(wrap, "h3", "dl-h3", "Map — cell size = reads, color = freshness"); - wrap.appendChild(insightsTreemap(pts)); - - el(wrap, "h3", "dl-h3", "Reads × freshness"); - wrap.appendChild(insightsChart(pts)); - - el(wrap, "h3", "dl-h3", "Hot path — top files by reads"); - renderHotPath(wrap, pts, lens); - - if (insightsDevices && insightsDevices.length) { - el(wrap, "h3", "dl-h3", "Agent coverage — which agents read which areas"); - wrap.appendChild(insightsMatrix(insightsDevices)); - } -} - -/* ---- treemap (the landing chart) ---- - Squarified treemap (Bruls et al.), dependency-free: items sorted by value - fill a rect in rows along the shorter side, keeping cells near-square. - Returns [{item, x, y, w, h}]. */ -function squarify(items, x, y, w, h) { - const total = items.reduce((s, it) => s + it.value, 0); - if (!total || w <= 0 || h <= 0) return []; - let rest = items.slice().sort((a, b) => b.value - a.value) - .map((it) => ({ it, a: (it.value / total) * w * h })); - const worst = (row, side) => { - const sum = row.reduce((t, r) => t + r.a, 0); - const d = sum / side; - let m = 0; - for (const r of row) { - const l = r.a / d; - m = Math.max(m, l / d, d / l); - } - return m; - }; - const out = []; - while (rest.length) { - const horiz = w >= h; // row = a strip along the shorter side - const side = horiz ? h : w; - const row = [rest.shift()]; - while (rest.length && worst(row.concat(rest[0]), side) <= worst(row, side)) { - row.push(rest.shift()); - } - const d = row.reduce((t, r) => t + r.a, 0) / side; - let off = 0; - for (const r of row) { - const l = r.a / d; - if (horiz) out.push({ item: r.it, x, y: y + off, w: d, h: l }); - else out.push({ item: r.it, x: x + off, y, w: l, h: d }); - off += l; - } - if (horiz) { x += d; w -= d; } else { y += d; h -= d; } - } - return out; -} - -const TM_HEADER = 15; // group label strip height - -function insightsTreemap(pts) { - const W = 720, H = 480; - const svg = svgEl(null, "svg", { viewBox: `0 0 ${W} ${H}`, class: "in-chart in-treemap" }); - // Two levels: top-level folder groups, files within each. - const groups = new Map(); - for (const p of pts) { - const top = p.path.includes("/") ? p.path.split("/")[0] : "/"; - let g = groups.get(top); - if (!g) groups.set(top, g = { name: top, files: [], value: 0 }); - g.files.push(p); - g.value += p.reads + 1; // +1: unread files still occupy a sliver - } - for (const gc of squarify([...groups.values()], 0, 0, W, H)) { - const g = gc.item; - const dir = g.name === "/" ? "" : g.name; - svgEl(svg, "rect", { - x: gc.x + 1, y: gc.y + 1, width: Math.max(0, gc.w - 2), height: Math.max(0, gc.h - 2), - rx: 3, class: "in-tm-group", "data-dir": dir, - }); - if (gc.w > 46 && gc.h > TM_HEADER + 10) { - let label = g.name === "/" ? "(root)" : g.name; - const fit = Math.floor((gc.w - 8) / 6); - if (label.length > fit) label = label.slice(0, Math.max(1, fit - 1)) + "…"; - svgEl(svg, "text", { x: gc.x + 5, y: gc.y + 12, class: "in-tm-glabel", "data-dir": dir }, label); - } - const cells = squarify( - g.files.map((f) => ({ ...f, name: f.path.split("/").pop(), value: f.reads + 1 })), - gc.x + 2, gc.y + TM_HEADER, Math.max(0, gc.w - 4), Math.max(0, gc.h - TM_HEADER - 2)); - for (const c of cells) { - const cell = svgEl(svg, "rect", { - x: c.x + 0.6, y: c.y + 0.6, width: Math.max(0.4, c.w - 1.2), height: Math.max(0.4, c.h - 1.2), - rx: 1.5, fill: staleColor(c.item.days), class: "in-tm-cell", "data-path": c.item.path, - }); - svgEl(cell, "title", {}, - `${c.item.path} — ${c.item.reads} read${c.item.reads === 1 ? "" : "s"}/30d · changed ${Math.round(c.item.days)}d ago`); - if (c.w > 54 && c.h > 16) { - const fit = Math.floor((c.w - 8) / 6); - let label = (c.item.danger ? "⚠ " : "") + c.item.name; - if (label.length > fit) label = label.slice(0, Math.max(1, fit - 1)) + "…"; - if (fit >= 5) svgEl(svg, "text", { x: c.x + 4.5, y: c.y + 12.5, class: "in-tm-label", "data-path": c.item.path }, label); - } - } - } - // One delegated click handler for thousands of cells. - svg.addEventListener("click", (e) => { - const t = e.target.closest("[data-path], [data-dir]"); - if (!t) return; - const path = t.getAttribute("data-path"); - if (path) return openFile(path); - const dir = t.getAttribute("data-dir"); - if (dir && dirIndex.has(dir)) openFolder(dir); - }); - return svg; -} - -/* ---- hot path: top-20 files by reads, agent/human split ---- */ -function renderHotPath(wrap, pts, lens) { - const top = pts.filter((p) => p.reads > 0) - .sort((a, b) => b.reads - a.reads || b.days - a.days).slice(0, 20); - if (!top.length) { - el(wrap, "div", "dl-empty", "No reads in the window yet."); - return; - } - const max = top[0].reads; - const list = el(wrap, "div", "in-hotpath"); - for (const p of top) { - const row = el(list, "div", "in-hp-row"); - row.tabIndex = 0; - row.setAttribute("role", "button"); - row.title = p.danger - ? `${p.reads} read${p.reads === 1 ? "" : "s"}/30d · unchanged ${Math.round(p.days)}d — review this file` - : p.path; - el(row, "span", "in-hp-name" + (p.danger ? " danger" : ""), p.path + (p.danger ? " ⚠" : "")); - const barw = el(row, "span", "in-hp-bar"); - // Split of the lens reads: pure lenses are single-color by definition. - const aFrac = lens === "agent" ? 1 : lens === "human" ? 0 : (p.total ? p.agent / p.total : 0); - const pct = (p.reads / max) * 100; - el(barw, "span", "in-hp-agent").style.width = (pct * aFrac).toFixed(1) + "%"; - el(barw, "span", "in-hp-human").style.width = (pct * (1 - aFrac)).toFixed(1) + "%"; - el(row, "span", "in-hp-count", p.reads); - row.onclick = () => openFile(p.path); - row.onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); row.click(); } }; - } - const lg = el(wrap, "p", "in-legend"); - el(lg, "span", "in-sw agent"); - lg.append(" agent reads "); - el(lg, "span", "in-sw human"); - lg.append(" human reads"); -} - -/* ---- agent coverage matrix: devices × top-level folders ---- */ -function insightsMatrix(devices) { - const totals = new Map(); - for (const d of devices) { - for (const [f, n] of Object.entries(d.folders || {})) totals.set(f, (totals.get(f) || 0) + n); - } - const cols = [...totals.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12).map((e) => e[0]); - const rows = devices.slice(0, 12); // server sorts by total desc - const left = 140, top = 6, cw = Math.min(76, Math.max(34, (720 - left - 8) / cols.length)), ch = 26; - const W = 720, H = top + rows.length * ch + 58; - const svg = svgEl(null, "svg", { viewBox: `0 0 ${W} ${H}`, class: "in-chart in-matrix" }); - const max = Math.max(1, ...rows.flatMap((d) => cols.map((c) => (d.folders || {})[c] || 0))); - const shade = (t) => { // #17191f → amber by intensity - const a = [23, 25, 31], b = [245, 166, 35]; - const c = a.map((v, i) => Math.round(v + (b[i] - v) * t)); - return `rgb(${c[0]},${c[1]},${c[2]})`; - }; - rows.forEach((d, i) => { - let label = d.name || d.id; - if (label.length > 20) label = label.slice(0, 19) + "…"; - svgEl(svg, "text", { x: left - 8, y: top + i * ch + 17, "text-anchor": "end", class: "in-label" }, label); - cols.forEach((c, j) => { - const v = (d.folders || {})[c] || 0; - const cell = svgEl(svg, "rect", { - x: left + j * cw, y: top + i * ch, width: cw - 4, height: ch - 4, rx: 3, - fill: shade(Math.sqrt(v / max)), - }); - svgEl(cell, "title", {}, `${d.name || d.id} × ${c || "(root)"}: ${v} read${v === 1 ? "" : "s"}/30d`); - }); - }); - cols.forEach((c, j) => { - const cx = left + j * cw + (cw - 4) / 2, cy = top + rows.length * ch + 14; - svgEl(svg, "text", { - x: cx, y: cy, class: "in-label", "text-anchor": "end", - transform: `rotate(-28 ${cx} ${cy})`, - }, c || "(root)"); - }); - return svg; -} - -/* Dependency-free SVG scatter: x = days since last write, y = reads, both - log-scaled; threshold lines split the quadrants. */ -function insightsChart(pts) { - const W = 720, H = 360, M = { l: 44, r: 16, t: 20, b: 34 }; - const maxDays = Math.max(STALE_DAYS * 2, ...pts.map((p) => p.days)); - const maxReads = Math.max(HOT_READS * 2, ...pts.map((p) => p.reads)); - const lx = (d) => Math.log10(d + 1) / Math.log10(maxDays + 1); - const ly = (r) => Math.log10(r + 1) / Math.log10(maxReads + 1); - const X = (d) => M.l + lx(d) * (W - M.l - M.r); - const Y = (r) => H - M.b - ly(r) * (H - M.t - M.b); - - const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg.setAttribute("viewBox", `0 0 ${W} ${H}`); - svg.setAttribute("class", "in-chart"); - const add = (tag, attrs, text) => { - const n = document.createElementNS("http://www.w3.org/2000/svg", tag); - for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, v); - if (text != null) n.textContent = text; - svg.appendChild(n); - return n; - }; - - // danger quadrant shading + threshold lines - add("rect", { x: X(STALE_DAYS), y: M.t, width: W - M.r - X(STALE_DAYS), height: Y(HOT_READS) - M.t, class: "in-danger-zone" }); - add("line", { x1: X(STALE_DAYS), y1: M.t, x2: X(STALE_DAYS), y2: H - M.b, class: "in-threshold" }); - add("line", { x1: M.l, y1: Y(HOT_READS), x2: W - M.r, y2: Y(HOT_READS), class: "in-threshold" }); - // axes - add("line", { x1: M.l, y1: H - M.b, x2: W - M.r, y2: H - M.b, class: "in-axis" }); - add("line", { x1: M.l, y1: M.t, x2: M.l, y2: H - M.b, class: "in-axis" }); - add("text", { x: (M.l + W - M.r) / 2, y: H - 8, class: "in-label" }, "days since last change →"); - add("text", { x: 12, y: (M.t + H - M.b) / 2, class: "in-label", transform: `rotate(-90 12 ${(M.t + H - M.b) / 2})` }, "reads / 30d →"); - add("text", { x: W - M.r - 6, y: M.t + 14, class: "in-quad in-quad-danger", "text-anchor": "end" }, "hot + stale"); - add("text", { x: M.l + 6, y: M.t + 14, class: "in-quad" }, "hot + fresh"); - add("text", { x: W - M.r - 6, y: H - M.b - 8, class: "in-quad", "text-anchor": "end" }, "cold + stale"); - add("text", { x: W - M.r - 6, y: M.t + 28, class: "in-label", "text-anchor": "end" }, "dot size = agent share of reads"); - - for (const p of pts) { - // Radius encodes the agent share of the file's reads; translucent dots - // keep the cloud readable at hundreds of files. - const share = p.total ? (p.agent || 0) / p.total : 0; - const c = add("circle", { - cx: X(p.days).toFixed(1), cy: Y(p.reads).toFixed(1), r: (3 + 4 * share).toFixed(1), - class: "in-pt" + (p.danger ? " danger" : p.reads ? "" : " cold"), - }); - const tip = document.createElementNS("http://www.w3.org/2000/svg", "title"); - tip.textContent = `${p.path} — ${p.reads} read${p.reads === 1 ? "" : "s"} / 30d · changed ${Math.round(p.days)}d ago`; - c.appendChild(tip); - c.onclick = () => openFile(p.path); - } - return svg; -} - -/* ---- history ---- - Every change ever made, straight from the journals: who (account), when, - from which device (name, OS, IP as the server saw it), with view/download - of that exact version. Groundwork for revert/rollback. */ -function initHistory() { - const btn = $("history-btn"); - btn.hidden = !(serverConfig.mode === "hub" && currentProjectOrNull()); - btn.onclick = () => { - if (!currentPath) return showHistory({ prefix: "" }); - if (dirIndex.has(currentPath)) return showHistory({ prefix: currentPath + "/" }); - showHistory({ path: currentPath }); - }; -} - -function currentProjectOrNull() { - return serverConfig.mode === "hub" ? currentProject : null; -} - -async function showHistory(q, push = true) { - // A real route (?v=history&path=|prefix=): refresh and back/forward work. - if (push) pushURL(urlForView("history", q)); - const restoreTop = pendingScroll != null ? pendingScroll : 0; - pendingScroll = null; - const content = $("content"); - const qs = "path" in q - ? "path=" + encodeURIComponent(q.path) - : "prefix=" + encodeURIComponent(q.prefix); - let out; - try { - out = await getJSON(apiBase + "history?" + qs + "&n=200"); - } catch (err) { - $("meta").textContent = "History unavailable: " + err.message; - return; - } - const title = "path" in q ? q.path : (q.prefix ? q.prefix + " (folder)" : "all changes"); - $("crumb").textContent = "History — " + title; - $("meta").textContent = ""; - $("download").hidden = true; - content.className = "view"; - $("more-btn").hidden = true; - content.innerHTML = ""; - const wrap = document.createElement("div"); - wrap.className = "history"; - if (!out.entries || out.entries.length === 0) { - wrap.innerHTML = `
No history yet.
`; - } - for (const e of out.entries || []) wrap.appendChild(historyEntryRow(e)); - content.appendChild(wrap); - content.scrollTo({ top: restoreTop, behavior: "instant" }); -} - -/* One change as a row: what happened (added / edited / deleted), to which - file, by whom, from where — with view/download of that exact version. */ -const KIND_ICON = { add: "plus", edit: "edit", delete: "x" }; -const KIND_LABEL = { add: "added", edit: "edited", delete: "deleted" }; -function historyEntryRow(e) { - const kind = e.kind === "put" ? "edit" : e.kind; // older servers report raw "put" ops - const row = document.createElement("div"); - row.className = "hentry " + kind; - const who = e.user_name ? `${e.user_name} <${e.user}>` : (e.user || e.author || "unknown"); - const dev = [e.device.name || e.device.id, e.device.os, e.device.ip].filter(Boolean).join(" · "); - const when = new Date(e.time).toLocaleString(); - row.innerHTML = - `
` + - `
`; - row.querySelector(".hkind").innerHTML = svgIcon(KIND_ICON[kind] || "dot"); - row.querySelector(".hpath").textContent = e.path; - row.querySelector(".htag").textContent = KIND_LABEL[kind] || kind; - row.querySelector(".htime").textContent = when; - row.querySelector(".hwho").textContent = who; - row.querySelector(".hdev").textContent = dev; - row.querySelector(".hsize").textContent = e.size ? humanSize(e.size) : ""; - if (e.note) { - const note = document.createElement("div"); - note.className = "hnote"; - // Linkify http(s) URLs (e.g. a Claude session link); everything else - // stays plain text — notes are user/agent input, never markup. - for (const tok of e.note.split(/(https?:\/\/\S+)/)) { - if (/^https?:\/\//.test(tok)) { - const a = document.createElement("a"); - a.href = tok; a.textContent = tok; a.target = "_blank"; a.rel = "noopener"; - note.appendChild(a); - } else if (tok) { - note.append(tok); - } - } - // Long notes clamp to one line; clicking the note (links still work) - // expands to the full text and back. - note.tabIndex = 0; - note.setAttribute("role", "button"); - note.title = "Show full note"; - note.onclick = (ev) => { - ev.stopPropagation(); // expanding a note is not a navigation - if (ev.target.tagName === "A") return; - const open = note.classList.toggle("open"); - note.title = open ? "Collapse note" : "Show full note"; - note.setAttribute("aria-expanded", String(open)); - }; - note.onkeydown = (ev) => { if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); note.click(); } }; - row.appendChild(note); - } - // The whole row opens the file it names (deleted files have nothing to - // open). Old versions stay retrievable via the blob API; a proper version - // viewer is the planned time-travel phase. - const p = e.path; - if (kind !== "delete") { - row.classList.add("clickable"); - row.tabIndex = 0; - row.setAttribute("role", "button"); - row.onclick = (ev) => { if (ev.target.tagName !== "A") openFile(p); }; - row.onkeydown = (ev) => { if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); openFile(p); } }; - } - return row; -} - -function humanSize(n) { - if (n < 1024) return n + " B"; - const units = ["KB", "MB", "GB", "TB"]; - let i = -1; - do { n /= 1024; i++; } while (n >= 1024 && i < units.length - 1); - return n.toFixed(1) + " " + units[i]; -} - -/* ---- upload ---- - The client asks the server how to upload (upload/init): "direct" hands - back a short-lived presigned URL and the bytes go straight to the object - store; "server" means relay the bytes through the bdrive server. */ -function initUpload() { - const btn = $("upload-btn"); - const enabled = serverConfig.upload && serverConfig.upload.enabled && - (serverConfig.mode !== "hub" || currentProject); - if (!enabled) { btn.hidden = true; return; } - btn.hidden = false; - const input = $("upload-input"); - btn.onclick = () => input.click(); - input.onchange = async () => { - const file = input.files[0]; - input.value = ""; - if (!file) return; - // A selected folder receives the upload; a selected file means "next to it". - const dir = !currentPath ? "" - : dirIndex.has(currentPath) ? currentPath - : currentPath.includes("/") ? currentPath.slice(0, currentPath.lastIndexOf("/")) : ""; - const dest = dir ? dir + "/" + file.name : file.name; - const status = $("meta"); - try { - status.textContent = `Uploading ${dest}…`; - await uploadFile(dest, file); - status.textContent = `Uploaded ${dest}`; - await refreshTree(); - openFile(dest); - } catch (err) { - status.textContent = "Upload failed: " + err.message; - } - }; -} - -async function uploadFile(dest, file) { - const buf = await file.arrayBuffer(); - const sha = await sha256Hex(buf); - const post = async (url, body) => { - const r = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - if (!r.ok) throw new Error(await r.text()); - return r.json(); - }; - const req = { path: dest, sha256: sha, size: file.size }; - const plan = await post(apiBase + "upload/init", req); - if (plan.mode === "direct") { - if (!plan.exists) { // identical content already in the store? skip the PUT - const r = await fetch(plan.url, { method: plan.method || "PUT", headers: plan.headers || {}, body: buf }); - if (!r.ok) throw new Error("storage upload failed: " + r.status); - } - await post(apiBase + "upload/commit", req); - } else { - const r = await fetch(apiBase + "upload/content?path=" + encodeURIComponent(dest), { - method: "PUT", body: buf, - }); - if (!r.ok) throw new Error(await r.text()); - } -} - -async function sha256Hex(buf) { - if (crypto.subtle) { - const d = await crypto.subtle.digest("SHA-256", buf); - return [...new Uint8Array(d)].map((b) => b.toString(16).padStart(2, "0")).join(""); - } - return sha256Fallback(new Uint8Array(buf)); // plain-http origins have no crypto.subtle -} - -/* Minimal SHA-256 (FIPS 180-4) for non-secure contexts. */ -function sha256Fallback(bytes) { - const K = new Uint32Array([ - 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, - 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, - 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, - 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, - 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, - 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, - 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, - 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2]); - const H = new Uint32Array([ - 0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]); - const rr = (x, n) => (x >>> n) | (x << (32 - n)); - const len = bytes.length; - const padded = new Uint8Array((((len + 8) >> 6) + 1) << 6); - padded.set(bytes); - padded[len] = 0x80; - const dv = new DataView(padded.buffer); - dv.setUint32(padded.length - 8, Math.floor((len * 8) / 0x100000000)); - dv.setUint32(padded.length - 4, (len * 8) >>> 0); - const w = new Uint32Array(64); - for (let off = 0; off < padded.length; off += 64) { - for (let i = 0; i < 16; i++) w[i] = dv.getUint32(off + i * 4); - for (let i = 16; i < 64; i++) { - const s0 = rr(w[i-15],7) ^ rr(w[i-15],18) ^ (w[i-15] >>> 3); - const s1 = rr(w[i-2],17) ^ rr(w[i-2],19) ^ (w[i-2] >>> 10); - w[i] = (w[i-16] + s0 + w[i-7] + s1) >>> 0; - } - let [a,b,c,d,e,f,g,h] = H; - for (let i = 0; i < 64; i++) { - const S1 = rr(e,6) ^ rr(e,11) ^ rr(e,25); - const t1 = (h + S1 + ((e & f) ^ (~e & g)) + K[i] + w[i]) >>> 0; - const S0 = rr(a,2) ^ rr(a,13) ^ rr(a,22); - const t2 = (S0 + ((a & b) ^ (a & c) ^ (b & c))) >>> 0; - h=g; g=f; f=e; e=(d+t1)>>>0; d=c; c=b; b=a; a=(t1+t2)>>>0; - } - H[0]+=a; H[1]+=b; H[2]+=c; H[3]+=d; H[4]+=e; H[5]+=f; H[6]+=g; H[7]+=h; - } - return [...H].map((x) => (x >>> 0).toString(16).padStart(8, "0")).join(""); -} - -/* ---- command palette (⌘K / Ctrl+K) ---- - One box for everything: fuzzy-jump to any file, switch projects, and run - quick actions (share, history, upload, download, sign out). */ -let paletteItems = []; -let paletteSel = 0; - -function paletteOpen() { - buildPaletteItems(""); - $("palette-overlay").hidden = false; - const input = $("palette-input"); - input.value = ""; - input.focus(); -} - -function paletteClose() { - $("palette-overlay").hidden = true; -} - -/* Subsequence fuzzy match. Returns a score (higher = better) or -1, plus - the matched positions for highlighting. */ -function fuzzy(query, text) { - if (!query) return { score: 0, hits: [] }; - const q = query.toLowerCase(); - const t = text.toLowerCase(); - let ti = 0, score = 0, streak = 0; - const hits = []; - for (let qi = 0; qi < q.length; qi++) { - const found = t.indexOf(q[qi], ti); - if (found === -1) return null; - streak = found === ti ? streak + 1 : 1; - score += streak * 3; // consecutive runs - if (found === 0 || "/ -_.".includes(t[found - 1])) score += 8; // word starts - hits.push(found); - ti = found + 1; - } - score -= Math.floor(t.length / 8); // mild preference for short targets - return { score, hits }; -} - -function highlight(text, hits) { - const span = document.createElement("span"); - span.className = "plabel"; - let last = 0; - for (const h of hits) { - if (h > last) span.append(text.slice(last, h)); - const b = document.createElement("b"); - b.textContent = text[h]; - span.append(b); - last = h + 1; - } - span.append(text.slice(last)); - return span; -} - -/* Candidate sources: context-aware actions, then projects, then files. */ -function paletteCandidates() { - const items = []; - const add = (icon, label, kind, run) => items.push({ icon, label, kind, run }); - const hub = serverConfig.mode === "hub"; - - if (hub && currentProject && currentPath) { - add("share", "Share: " + currentPath, "action", () => $("share-btn").click()); - add("hist", "History: " + currentPath, "action", () => showHistory({ path: currentPath })); - add("download", "Download: " + currentPath, "action", () => $("download").click()); - } - if (hub && currentProject) { - add("hist", "History: whole project", "action", () => showHistory({ prefix: "" })); - } - if (!$("upload-btn").hidden) { - add("upload", "Upload a file…", "action", () => $("upload-btn").click()); - } - if (hub) { - for (const p of projects) { - if (!currentProject || p.id !== currentProject.id) { - add("folder", "Switch to project: " + p.name, "project", () => selectProject(p, null)); - } - } - } - if (serverConfig.auth && serverConfig.auth.enabled) { - add("power", "Sign out", "action", () => { location.href = "/auth/logout"; }); - } - for (const d of dirIndex.keys()) { - add("folder", d, "folder", () => openFolder(d)); - } - for (const f of flatFiles) { - add("doc", f.path, "file", () => openFile(f.path)); - } - return items; -} - -/* Match a query against a label, tolerating a simple English plural so - "ideas" still finds idea.md. Tries the raw query first, then a lightly - de-pluralized form (…ies→…y, …es→…, …s→…). */ -function fuzzyStemmed(query, label) { - const m = fuzzy(query, label); - if (m) return m; - const q = query.toLowerCase(); - let stem = null; - if (q.length > 3 && q.endsWith("ies")) stem = q.slice(0, -3) + "y"; - else if (q.length > 3 && q.endsWith("es")) stem = q.slice(0, -2); - else if (q.length > 2 && q.endsWith("s")) stem = q.slice(0, -1); - return stem ? fuzzy(stem, label) : null; -} - -function buildPaletteItems(query) { - const scored = []; - for (const c of paletteCandidates()) { - const m = fuzzyStemmed(query, c.label); - if (m) scored.push({ ...c, score: m.score, hits: m.hits }); - } - scored.sort((a, b) => b.score - a.score); - paletteItems = scored.slice(0, 40); - paletteSel = 0; - renderPalette(); -} - -function renderPalette() { - const ul = $("palette-results"); - ul.innerHTML = ""; - if (paletteItems.length === 0) { - const li = document.createElement("li"); - li.className = "pempty"; - li.textContent = "No matches — search covers file names, projects, and actions"; - ul.appendChild(li); - return; - } - paletteItems.forEach((item, i) => { - const li = document.createElement("li"); - if (i === paletteSel) li.classList.add("selected"); - const pic = document.createElement("span"); - pic.className = "picon"; - pic.innerHTML = svgIcon(item.icon); - const kind = document.createElement("span"); - kind.className = "pkind"; - kind.textContent = item.kind; - li.append(pic, highlight(item.label, item.hits), kind); - li.onclick = () => runPaletteItem(item); - li.onmousemove = () => { if (paletteSel !== i) { paletteSel = i; renderPalette(); } }; - ul.appendChild(li); - }); - const sel = ul.children[paletteSel]; - if (sel) sel.scrollIntoView({ block: "nearest" }); -} - -function runPaletteItem(item) { - paletteClose(); - item.run(); -} - -window.addEventListener("keydown", (e) => { - if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { - e.preventDefault(); - $("palette-overlay").hidden ? paletteOpen() : paletteClose(); - return; - } - if ($("palette-overlay").hidden) return; - if (e.key === "Escape") { - e.preventDefault(); - paletteClose(); - } else if (e.key === "ArrowDown" || e.key === "ArrowUp") { - e.preventDefault(); - const n = paletteItems.length; - if (n) { - paletteSel = (paletteSel + (e.key === "ArrowDown" ? 1 : n - 1)) % n; - renderPalette(); - } - } else if (e.key === "Enter") { - e.preventDefault(); - if (paletteItems[paletteSel]) runPaletteItem(paletteItems[paletteSel]); - } -}); - -$("palette-input").addEventListener("input", (e) => buildPaletteItems(e.target.value)); -$("palette-overlay").addEventListener("click", (e) => { - if (e.target.id === "palette-overlay") paletteClose(); -}); - -/* Visible search affordance in the top bar → opens the palette. */ -$("search-btn").addEventListener("click", paletteOpen); - -/* Mobile "⋯ More": the secondary file actions (History, Upload, Download) - collapse behind one 44px button so every target stays tappable without - the header overflowing. The menu proxies to the real buttons, so their - behavior and visibility rules are the single source of truth. */ -function buildMoreMenu() { - const menu = $("more-menu"); - menu.innerHTML = ""; - const items = [ - ["History", $("history-btn")], - ["Upload", $("upload-btn")], - ["Download", $("download")], - ].filter(([, el]) => el && !el.hidden); - for (const [label, el] of items) { - const b = document.createElement("button"); - b.className = "more-item"; - b.textContent = label; - b.onclick = () => { $("more-menu").hidden = true; el.click(); }; - menu.appendChild(b); - } - if (canSeeInsights()) { - const b = document.createElement("button"); - b.className = "more-item"; - b.textContent = "Insights"; - b.onclick = () => { $("more-menu").hidden = true; showInsights(); }; - menu.appendChild(b); - return items.length + 1; - } - return items.length; -} -$("more-btn").addEventListener("click", (e) => { - e.stopPropagation(); - const menu = $("more-menu"); - if (menu.hidden) { buildMoreMenu(); menu.hidden = false; } - else menu.hidden = true; -}); -document.addEventListener("click", () => { $("more-menu").hidden = true; }); - -/* Mobile: the sidebar is off-canvas; a hamburger toggles it. */ -function toggleSidebar() { document.body.classList.toggle("sb-open"); } -function closeSidebarOnMobile() { document.body.classList.remove("sb-open"); } -$("menu-btn").addEventListener("click", toggleSidebar); -$("sb-backdrop").addEventListener("click", closeSidebarOnMobile); - -/* Back/forward: re-resolve the route from the URL. selectProject/openFile - dedup against the current URL, so replaying it here never stacks history. */ -window.addEventListener("popstate", () => { - rememberScroll(); // routeKey still names the route we are leaving - routeKey = location.pathname + location.search; - pendingScroll = scrollMemo.has(routeKey) ? scrollMemo.get(routeKey) : null; - const r = parseRoute(); - if (serverConfig.mode === "hub" && r.project && (!currentProject || currentProject.id !== r.project)) { - const proj = projects.find((x) => x.id === r.project); - if (proj) { selectProject(proj, r); return; } - } - applyRoute(r); -}); - -boot(); diff --git a/internal/webapp/static/assets/index-CZjUv2DS.js b/internal/webapp/static/assets/index-CZjUv2DS.js new file mode 100644 index 0000000..0de0955 --- /dev/null +++ b/internal/webapp/static/assets/index-CZjUv2DS.js @@ -0,0 +1,11 @@ +(function(){const c=document.createElement("link").relList;if(c&&c.supports&&c.supports("modulepreload"))return;for(const h of document.querySelectorAll('link[rel="modulepreload"]'))s(h);new MutationObserver(h=>{for(const d of h)if(d.type==="childList")for(const v of d.addedNodes)v.tagName==="LINK"&&v.rel==="modulepreload"&&s(v)}).observe(document,{childList:!0,subtree:!0});function r(h){const d={};return h.integrity&&(d.integrity=h.integrity),h.referrerPolicy&&(d.referrerPolicy=h.referrerPolicy),h.crossOrigin==="use-credentials"?d.credentials="include":h.crossOrigin==="anonymous"?d.credentials="omit":d.credentials="same-origin",d}function s(h){if(h.ep)return;h.ep=!0;const d=r(h);fetch(h.href,d)}})();var jf={exports:{}},Nn={};var vd;function wv(){if(vd)return Nn;vd=1;var u=Symbol.for("react.transitional.element"),c=Symbol.for("react.fragment");function r(s,h,d){var v=null;if(d!==void 0&&(v=""+d),h.key!==void 0&&(v=""+h.key),"key"in h){d={};for(var E in h)E!=="key"&&(d[E]=h[E])}else d=h;return h=d.ref,{$$typeof:u,type:s,key:v,ref:h!==void 0?h:null,props:d}}return Nn.Fragment=c,Nn.jsx=r,Nn.jsxs=r,Nn}var gd;function Zv(){return gd||(gd=1,jf.exports=wv()),jf.exports}var Lt=Zv(),Lf={exports:{}},k={};var pd;function Vv(){if(pd)return k;pd=1;var u=Symbol.for("react.transitional.element"),c=Symbol.for("react.portal"),r=Symbol.for("react.fragment"),s=Symbol.for("react.strict_mode"),h=Symbol.for("react.profiler"),d=Symbol.for("react.consumer"),v=Symbol.for("react.context"),E=Symbol.for("react.forward_ref"),y=Symbol.for("react.suspense"),g=Symbol.for("react.memo"),T=Symbol.for("react.lazy"),R=Symbol.for("react.activity"),N=Symbol.iterator;function j(S){return S===null||typeof S!="object"?null:(S=N&&S[N]||S["@@iterator"],typeof S=="function"?S:null)}var Q={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},B=Object.assign,q={};function X(S,H,Y){this.props=S,this.context=H,this.refs=q,this.updater=Y||Q}X.prototype.isReactComponent={},X.prototype.setState=function(S,H){if(typeof S!="object"&&typeof S!="function"&&S!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,S,H,"setState")},X.prototype.forceUpdate=function(S){this.updater.enqueueForceUpdate(this,S,"forceUpdate")};function J(){}J.prototype=X.prototype;function V(S,H,Y){this.props=S,this.context=H,this.refs=q,this.updater=Y||Q}var it=V.prototype=new J;it.constructor=V,B(it,X.prototype),it.isPureReactComponent=!0;var ct=Array.isArray;function bt(){}var F={H:null,A:null,T:null,S:null},st=Object.prototype.hasOwnProperty;function zt(S,H,Y){var w=Y.ref;return{$$typeof:u,type:S,key:H,ref:w!==void 0?w:null,props:Y}}function Wt(S,H){return zt(S.type,H,S.props)}function kt(S){return typeof S=="object"&&S!==null&&S.$$typeof===u}function _t(S){var H={"=":"=0",":":"=2"};return"$"+S.replace(/[=:]/g,function(Y){return H[Y]})}var It=/\/+/g;function be(S,H){return typeof S=="object"&&S!==null&&S.key!=null?_t(""+S.key):H.toString(36)}function Nt(S){switch(S.status){case"fulfilled":return S.value;case"rejected":throw S.reason;default:switch(typeof S.status=="string"?S.then(bt,bt):(S.status="pending",S.then(function(H){S.status==="pending"&&(S.status="fulfilled",S.value=H)},function(H){S.status==="pending"&&(S.status="rejected",S.reason=H)})),S.status){case"fulfilled":return S.value;case"rejected":throw S.reason}}throw S}function D(S,H,Y,w,I){var et=typeof S;(et==="undefined"||et==="boolean")&&(S=null);var dt=!1;if(S===null)dt=!0;else switch(et){case"bigint":case"string":case"number":dt=!0;break;case"object":switch(S.$$typeof){case u:case c:dt=!0;break;case T:return dt=S._init,D(dt(S._payload),H,Y,w,I)}}if(dt)return I=I(S),dt=w===""?"."+be(S,0):w,ct(I)?(Y="",dt!=null&&(Y=dt.replace(It,"$&/")+"/"),D(I,H,Y,"",function(La){return La})):I!=null&&(kt(I)&&(I=Wt(I,Y+(I.key==null||S&&S.key===I.key?"":(""+I.key).replace(It,"$&/")+"/")+dt)),H.push(I)),1;dt=0;var Pt=w===""?".":w+":";if(ct(S))for(var Dt=0;Dt>>1,Et=D[vt];if(0>>1;vth(Y,W))wh(I,Y)?(D[vt]=I,D[w]=W,vt=w):(D[vt]=Y,D[H]=W,vt=H);else if(wh(I,W))D[vt]=I,D[w]=W,vt=w;else break t}}return L}function h(D,L){var W=D.sortIndex-L.sortIndex;return W!==0?W:D.id-L.id}if(u.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var d=performance;u.unstable_now=function(){return d.now()}}else{var v=Date,E=v.now();u.unstable_now=function(){return v.now()-E}}var y=[],g=[],T=1,R=null,N=3,j=!1,Q=!1,B=!1,q=!1,X=typeof setTimeout=="function"?setTimeout:null,J=typeof clearTimeout=="function"?clearTimeout:null,V=typeof setImmediate<"u"?setImmediate:null;function it(D){for(var L=r(g);L!==null;){if(L.callback===null)s(g);else if(L.startTime<=D)s(g),L.sortIndex=L.expirationTime,c(y,L);else break;L=r(g)}}function ct(D){if(B=!1,it(D),!Q)if(r(y)!==null)Q=!0,bt||(bt=!0,_t());else{var L=r(g);L!==null&&Nt(ct,L.startTime-D)}}var bt=!1,F=-1,st=5,zt=-1;function Wt(){return q?!0:!(u.unstable_now()-ztD&&Wt());){var vt=R.callback;if(typeof vt=="function"){R.callback=null,N=R.priorityLevel;var Et=vt(R.expirationTime<=D);if(D=u.unstable_now(),typeof Et=="function"){R.callback=Et,it(D),L=!0;break e}R===r(y)&&s(y),it(D)}else s(y);R=r(y)}if(R!==null)L=!0;else{var S=r(g);S!==null&&Nt(ct,S.startTime-D),L=!1}}break t}finally{R=null,N=W,j=!1}L=void 0}}finally{L?_t():bt=!1}}}var _t;if(typeof V=="function")_t=function(){V(kt)};else if(typeof MessageChannel<"u"){var It=new MessageChannel,be=It.port2;It.port1.onmessage=kt,_t=function(){be.postMessage(null)}}else _t=function(){X(kt,0)};function Nt(D,L){F=X(function(){D(u.unstable_now())},L)}u.unstable_IdlePriority=5,u.unstable_ImmediatePriority=1,u.unstable_LowPriority=4,u.unstable_NormalPriority=3,u.unstable_Profiling=null,u.unstable_UserBlockingPriority=2,u.unstable_cancelCallback=function(D){D.callback=null},u.unstable_forceFrameRate=function(D){0>D||125vt?(D.sortIndex=W,c(g,D),r(y)===null&&D===r(g)&&(B?(J(F),F=-1):B=!0,Nt(ct,W-vt))):(D.sortIndex=Et,c(y,D),Q||j||(Q=!0,bt||(bt=!0,_t()))),D},u.unstable_shouldYield=Wt,u.unstable_wrapCallback=function(D){var L=N;return function(){var W=N;N=L;try{return D.apply(this,arguments)}finally{N=W}}}})(Xf)),Xf}var Ed;function Jv(){return Ed||(Ed=1,Gf.exports=Kv()),Gf.exports}var wf={exports:{}},$t={};var Td;function Fv(){if(Td)return $t;Td=1;var u=es();function c(y){var g="https://react.dev/errors/"+y;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(u)}catch(c){console.error(c)}}return u(),wf.exports=Fv(),wf.exports}var Od;function Wv(){if(Od)return Hn;Od=1;var u=Jv(),c=es(),r=$v();function s(t){var e="https://react.dev/errors/"+t;if(1Et||(t.current=vt[Et],vt[Et]=null,Et--)}function Y(t,e){Et++,vt[Et]=t.current,t.current=e}var w=S(null),I=S(null),et=S(null),dt=S(null);function Pt(t,e){switch(Y(et,e),Y(I,t),Y(w,null),e.nodeType){case 9:case 11:t=(t=e.documentElement)&&(t=t.namespaceURI)?Lh(t):0;break;default:if(t=e.tagName,e=e.namespaceURI)e=Lh(e),t=Yh(e,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}H(w),Y(w,t)}function Dt(){H(w),H(I),H(et)}function La(t){t.memoizedState!==null&&Y(dt,t);var e=w.current,l=Yh(e,t.type);e!==l&&(Y(I,t),Y(w,l))}function Zn(t){I.current===t&&(H(w),H(I)),dt.current===t&&(H(dt),_n._currentValue=W)}var Si,ms;function Hl(t){if(Si===void 0)try{throw Error()}catch(l){var e=l.stack.trim().match(/\n( *(at )?)/);Si=e&&e[1]||"",ms=-1)":-1n||m[a]!==A[n]){var _=` +`+m[a].replace(" at new "," at ");return t.displayName&&_.includes("")&&(_=_.replace("",t.displayName)),_}while(1<=a&&0<=n);break}}}finally{bi=!1,Error.prepareStackTrace=l}return(l=t?t.displayName||t.name:"")?Hl(l):""}function bm(t,e){switch(t.tag){case 26:case 27:case 5:return Hl(t.type);case 16:return Hl("Lazy");case 13:return t.child!==e&&e!==null?Hl("Suspense Fallback"):Hl("Suspense");case 19:return Hl("SuspenseList");case 0:case 15:return Ei(t.type,!1);case 11:return Ei(t.type.render,!1);case 1:return Ei(t.type,!0);case 31:return Hl("Activity");default:return""}}function ys(t){try{var e="",l=null;do e+=bm(t,l),l=t,t=t.return;while(t);return e}catch(a){return` +Error generating stack: `+a.message+` +`+a.stack}}var Ti=Object.prototype.hasOwnProperty,Ri=u.unstable_scheduleCallback,Oi=u.unstable_cancelCallback,Em=u.unstable_shouldYield,Tm=u.unstable_requestPaint,se=u.unstable_now,Rm=u.unstable_getCurrentPriorityLevel,vs=u.unstable_ImmediatePriority,gs=u.unstable_UserBlockingPriority,Vn=u.unstable_NormalPriority,Om=u.unstable_LowPriority,ps=u.unstable_IdlePriority,Am=u.log,zm=u.unstable_setDisableYieldValue,Ya=null,re=null;function fl(t){if(typeof Am=="function"&&zm(t),re&&typeof re.setStrictMode=="function")try{re.setStrictMode(Ya,t)}catch{}}var oe=Math.clz32?Math.clz32:_m,Mm=Math.log,Cm=Math.LN2;function _m(t){return t>>>=0,t===0?32:31-(Mm(t)/Cm|0)|0}var Kn=256,Jn=262144,Fn=4194304;function ql(t){var e=t&42;if(e!==0)return e;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function $n(t,e,l){var a=t.pendingLanes;if(a===0)return 0;var n=0,i=t.suspendedLanes,f=t.pingedLanes;t=t.warmLanes;var o=a&134217727;return o!==0?(a=o&~i,a!==0?n=ql(a):(f&=o,f!==0?n=ql(f):l||(l=o&~t,l!==0&&(n=ql(l))))):(o=a&~i,o!==0?n=ql(o):f!==0?n=ql(f):l||(l=a&~t,l!==0&&(n=ql(l)))),n===0?0:e!==0&&e!==n&&(e&i)===0&&(i=n&-n,l=e&-e,i>=l||i===32&&(l&4194048)!==0)?e:n}function Ga(t,e){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&e)===0}function Dm(t,e){switch(t){case 1:case 2:case 4:case 8:case 64:return e+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Ss(){var t=Fn;return Fn<<=1,(Fn&62914560)===0&&(Fn=4194304),t}function Ai(t){for(var e=[],l=0;31>l;l++)e.push(t);return e}function Xa(t,e){t.pendingLanes|=e,e!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function Um(t,e,l,a,n,i){var f=t.pendingLanes;t.pendingLanes=l,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=l,t.entangledLanes&=l,t.errorRecoveryDisabledLanes&=l,t.shellSuspendCounter=0;var o=t.entanglements,m=t.expirationTimes,A=t.hiddenUpdates;for(l=f&~l;0"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var Qm=/[\n"\\]/g;function Te(t){return t.replace(Qm,function(e){return"\\"+e.charCodeAt(0).toString(16)+" "})}function Ui(t,e,l,a,n,i,f,o){t.name="",f!=null&&typeof f!="function"&&typeof f!="symbol"&&typeof f!="boolean"?t.type=f:t.removeAttribute("type"),e!=null?f==="number"?(e===0&&t.value===""||t.value!=e)&&(t.value=""+Ee(e)):t.value!==""+Ee(e)&&(t.value=""+Ee(e)):f!=="submit"&&f!=="reset"||t.removeAttribute("value"),e!=null?xi(t,f,Ee(e)):l!=null?xi(t,f,Ee(l)):a!=null&&t.removeAttribute("value"),n==null&&i!=null&&(t.defaultChecked=!!i),n!=null&&(t.checked=n&&typeof n!="function"&&typeof n!="symbol"),o!=null&&typeof o!="function"&&typeof o!="symbol"&&typeof o!="boolean"?t.name=""+Ee(o):t.removeAttribute("name")}function xs(t,e,l,a,n,i,f,o){if(i!=null&&typeof i!="function"&&typeof i!="symbol"&&typeof i!="boolean"&&(t.type=i),e!=null||l!=null){if(!(i!=="submit"&&i!=="reset"||e!=null)){Di(t);return}l=l!=null?""+Ee(l):"",e=e!=null?""+Ee(e):l,o||e===t.value||(t.value=e),t.defaultValue=e}a=a??n,a=typeof a!="function"&&typeof a!="symbol"&&!!a,t.checked=o?t.checked:!!a,t.defaultChecked=!!a,f!=null&&typeof f!="function"&&typeof f!="symbol"&&typeof f!="boolean"&&(t.name=f),Di(t)}function xi(t,e,l){e==="number"&&In(t.ownerDocument)===t||t.defaultValue===""+l||(t.defaultValue=""+l)}function ua(t,e,l,a){if(t=t.options,e){e={};for(var n=0;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Qi=!1;if(we)try{var Ka={};Object.defineProperty(Ka,"passive",{get:function(){Qi=!0}}),window.addEventListener("test",Ka,Ka),window.removeEventListener("test",Ka,Ka)}catch{Qi=!1}var rl=null,ji=null,tu=null;function Ls(){if(tu)return tu;var t,e=ji,l=e.length,a,n="value"in rl?rl.value:rl.textContent,i=n.length;for(t=0;t=$a),Vs=" ",Ks=!1;function Js(t,e){switch(t){case"keyup":return oy.indexOf(e.keyCode)!==-1;case"keydown":return e.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Fs(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var sa=!1;function dy(t,e){switch(t){case"compositionend":return Fs(e);case"keypress":return e.which!==32?null:(Ks=!0,Vs);case"textInput":return t=e.data,t===Vs&&Ks?null:t;default:return null}}function my(t,e){if(sa)return t==="compositionend"||!wi&&Js(t,e)?(t=Ls(),tu=ji=rl=null,sa=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(e.ctrlKey||e.altKey||e.metaKey)||e.ctrlKey&&e.altKey){if(e.char&&1=e)return{node:l,offset:e-t};t=a}t:{for(;l;){if(l.nextSibling){l=l.nextSibling;break t}l=l.parentNode}l=void 0}l=lr(l)}}function nr(t,e){return t&&e?t===e?!0:t&&t.nodeType===3?!1:e&&e.nodeType===3?nr(t,e.parentNode):"contains"in t?t.contains(e):t.compareDocumentPosition?!!(t.compareDocumentPosition(e)&16):!1:!1}function ur(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var e=In(t.document);e instanceof t.HTMLIFrameElement;){try{var l=typeof e.contentWindow.location.href=="string"}catch{l=!1}if(l)t=e.contentWindow;else break;e=In(t.document)}return e}function Ki(t){var e=t&&t.nodeName&&t.nodeName.toLowerCase();return e&&(e==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||e==="textarea"||t.contentEditable==="true")}var Ty=we&&"documentMode"in document&&11>=document.documentMode,ra=null,Ji=null,Pa=null,Fi=!1;function ir(t,e,l){var a=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;Fi||ra==null||ra!==In(a)||(a=ra,"selectionStart"in a&&Ki(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),Pa&&Ia(Pa,a)||(Pa=a,a=Ju(Ji,"onSelect"),0>=f,n-=f,Be=1<<32-oe(e)+n|l<tt?(ut=Z,Z=null):ut=Z.sibling;var ot=z(b,Z,O[tt],U);if(ot===null){Z===null&&(Z=ut);break}t&&Z&&ot.alternate===null&&e(b,Z),p=i(ot,p,tt),rt===null?K=ot:rt.sibling=ot,rt=ot,Z=ut}if(tt===O.length)return l(b,Z),ft&&Ve(b,tt),K;if(Z===null){for(;tttt?(ut=Z,Z=null):ut=Z.sibling;var xl=z(b,Z,ot.value,U);if(xl===null){Z===null&&(Z=ut);break}t&&Z&&xl.alternate===null&&e(b,Z),p=i(xl,p,tt),rt===null?K=xl:rt.sibling=xl,rt=xl,Z=ut}if(ot.done)return l(b,Z),ft&&Ve(b,tt),K;if(Z===null){for(;!ot.done;tt++,ot=O.next())ot=x(b,ot.value,U),ot!==null&&(p=i(ot,p,tt),rt===null?K=ot:rt.sibling=ot,rt=ot);return ft&&Ve(b,tt),K}for(Z=a(Z);!ot.done;tt++,ot=O.next())ot=M(Z,b,tt,ot.value,U),ot!==null&&(t&&ot.alternate!==null&&Z.delete(ot.key===null?tt:ot.key),p=i(ot,p,tt),rt===null?K=ot:rt.sibling=ot,rt=ot);return t&&Z.forEach(function(Xv){return e(b,Xv)}),ft&&Ve(b,tt),K}function St(b,p,O,U){if(typeof O=="object"&&O!==null&&O.type===B&&O.key===null&&(O=O.props.children),typeof O=="object"&&O!==null){switch(O.$$typeof){case j:t:{for(var K=O.key;p!==null;){if(p.key===K){if(K=O.type,K===B){if(p.tag===7){l(b,p.sibling),U=n(p,O.props.children),U.return=b,b=U;break t}}else if(p.elementType===K||typeof K=="object"&&K!==null&&K.$$typeof===st&&Kl(K)===p.type){l(b,p.sibling),U=n(p,O.props),un(U,O),U.return=b,b=U;break t}l(b,p);break}else e(b,p);p=p.sibling}O.type===B?(U=Gl(O.props.children,b.mode,U,O.key),U.return=b,b=U):(U=ru(O.type,O.key,O.props,null,b.mode,U),un(U,O),U.return=b,b=U)}return f(b);case Q:t:{for(K=O.key;p!==null;){if(p.key===K)if(p.tag===4&&p.stateNode.containerInfo===O.containerInfo&&p.stateNode.implementation===O.implementation){l(b,p.sibling),U=n(p,O.children||[]),U.return=b,b=U;break t}else{l(b,p);break}else e(b,p);p=p.sibling}U=ec(O,b.mode,U),U.return=b,b=U}return f(b);case st:return O=Kl(O),St(b,p,O,U)}if(Nt(O))return G(b,p,O,U);if(_t(O)){if(K=_t(O),typeof K!="function")throw Error(s(150));return O=K.call(O),$(b,p,O,U)}if(typeof O.then=="function")return St(b,p,gu(O),U);if(O.$$typeof===V)return St(b,p,du(b,O),U);pu(b,O)}return typeof O=="string"&&O!==""||typeof O=="number"||typeof O=="bigint"?(O=""+O,p!==null&&p.tag===6?(l(b,p.sibling),U=n(p,O),U.return=b,b=U):(l(b,p),U=tc(O,b.mode,U),U.return=b,b=U),f(b)):l(b,p)}return function(b,p,O,U){try{nn=0;var K=St(b,p,O,U);return Ea=null,K}catch(Z){if(Z===ba||Z===yu)throw Z;var rt=de(29,Z,null,b.mode);return rt.lanes=U,rt.return=b,rt}}}var Fl=_r(!0),Dr=_r(!1),yl=!1;function dc(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function mc(t,e){t=t.updateQueue,e.updateQueue===t&&(e.updateQueue={baseState:t.baseState,firstBaseUpdate:t.firstBaseUpdate,lastBaseUpdate:t.lastBaseUpdate,shared:t.shared,callbacks:null})}function vl(t){return{lane:t,tag:0,payload:null,callback:null,next:null}}function gl(t,e,l){var a=t.updateQueue;if(a===null)return null;if(a=a.shared,(ht&2)!==0){var n=a.pending;return n===null?e.next=e:(e.next=n.next,n.next=e),a.pending=e,e=su(t),dr(t,null,l),e}return fu(t,a,e,l),su(t)}function cn(t,e,l){if(e=e.updateQueue,e!==null&&(e=e.shared,(l&4194048)!==0)){var a=e.lanes;a&=t.pendingLanes,l|=a,e.lanes=l,Es(t,l)}}function yc(t,e){var l=t.updateQueue,a=t.alternate;if(a!==null&&(a=a.updateQueue,l===a)){var n=null,i=null;if(l=l.firstBaseUpdate,l!==null){do{var f={lane:l.lane,tag:l.tag,payload:l.payload,callback:null,next:null};i===null?n=i=f:i=i.next=f,l=l.next}while(l!==null);i===null?n=i=e:i=i.next=e}else n=i=e;l={baseState:a.baseState,firstBaseUpdate:n,lastBaseUpdate:i,shared:a.shared,callbacks:a.callbacks},t.updateQueue=l;return}t=l.lastBaseUpdate,t===null?l.firstBaseUpdate=e:t.next=e,l.lastBaseUpdate=e}var vc=!1;function fn(){if(vc){var t=Sa;if(t!==null)throw t}}function sn(t,e,l,a){vc=!1;var n=t.updateQueue;yl=!1;var i=n.firstBaseUpdate,f=n.lastBaseUpdate,o=n.shared.pending;if(o!==null){n.shared.pending=null;var m=o,A=m.next;m.next=null,f===null?i=A:f.next=A,f=m;var _=t.alternate;_!==null&&(_=_.updateQueue,o=_.lastBaseUpdate,o!==f&&(o===null?_.firstBaseUpdate=A:o.next=A,_.lastBaseUpdate=m))}if(i!==null){var x=n.baseState;f=0,_=A=m=null,o=i;do{var z=o.lane&-536870913,M=z!==o.lane;if(M?(nt&z)===z:(a&z)===z){z!==0&&z===pa&&(vc=!0),_!==null&&(_=_.next={lane:0,tag:o.tag,payload:o.payload,callback:null,next:null});t:{var G=t,$=o;z=e;var St=l;switch($.tag){case 1:if(G=$.payload,typeof G=="function"){x=G.call(St,x,z);break t}x=G;break t;case 3:G.flags=G.flags&-65537|128;case 0:if(G=$.payload,z=typeof G=="function"?G.call(St,x,z):G,z==null)break t;x=R({},x,z);break t;case 2:yl=!0}}z=o.callback,z!==null&&(t.flags|=64,M&&(t.flags|=8192),M=n.callbacks,M===null?n.callbacks=[z]:M.push(z))}else M={lane:z,tag:o.tag,payload:o.payload,callback:o.callback,next:null},_===null?(A=_=M,m=x):_=_.next=M,f|=z;if(o=o.next,o===null){if(o=n.shared.pending,o===null)break;M=o,o=M.next,M.next=null,n.lastBaseUpdate=M,n.shared.pending=null}}while(!0);_===null&&(m=x),n.baseState=m,n.firstBaseUpdate=A,n.lastBaseUpdate=_,i===null&&(n.shared.lanes=0),Tl|=f,t.lanes=f,t.memoizedState=x}}function Ur(t,e){if(typeof t!="function")throw Error(s(191,t));t.call(e)}function xr(t,e){var l=t.callbacks;if(l!==null)for(t.callbacks=null,t=0;ti?i:8;var f=D.T,o={};D.T=o,qc(t,!1,e,l);try{var m=n(),A=D.S;if(A!==null&&A(o,m),m!==null&&typeof m=="object"&&typeof m.then=="function"){var _=Uy(m,a);hn(t,e,_,pe(t))}else hn(t,e,a,pe(t))}catch(x){hn(t,e,{then:function(){},status:"rejected",reason:x},pe())}finally{L.p=i,f!==null&&o.types!==null&&(f.types=o.types),D.T=f}}function Qy(){}function Nc(t,e,l,a){if(t.tag!==5)throw Error(s(476));var n=ro(t).queue;so(t,n,e,W,l===null?Qy:function(){return oo(t),l(a)})}function ro(t){var e=t.memoizedState;if(e!==null)return e;e={memoizedState:W,baseState:W,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:$e,lastRenderedState:W},next:null};var l={};return e.next={memoizedState:l,baseState:l,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:$e,lastRenderedState:l},next:null},t.memoizedState=e,t=t.alternate,t!==null&&(t.memoizedState=e),e}function oo(t){var e=ro(t);e.next===null&&(e=t.alternate.memoizedState),hn(t,e.next.queue,{},pe())}function Hc(){return Vt(_n)}function ho(){return xt().memoizedState}function mo(){return xt().memoizedState}function jy(t){for(var e=t.return;e!==null;){switch(e.tag){case 24:case 3:var l=pe();t=vl(l);var a=gl(e,t,l);a!==null&&(ce(a,e,l),cn(a,e,l)),e={cache:sc()},t.payload=e;return}e=e.return}}function Ly(t,e,l){var a=pe();l={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null},Cu(t)?vo(e,l):(l=Ii(t,e,l,a),l!==null&&(ce(l,t,a),go(l,e,a)))}function yo(t,e,l){var a=pe();hn(t,e,l,a)}function hn(t,e,l,a){var n={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null};if(Cu(t))vo(e,n);else{var i=t.alternate;if(t.lanes===0&&(i===null||i.lanes===0)&&(i=e.lastRenderedReducer,i!==null))try{var f=e.lastRenderedState,o=i(f,l);if(n.hasEagerState=!0,n.eagerState=o,he(o,f))return fu(t,e,n,0),Tt===null&&cu(),!1}catch{}if(l=Ii(t,e,n,a),l!==null)return ce(l,t,a),go(l,e,a),!0}return!1}function qc(t,e,l,a){if(a={lane:2,revertLane:mf(),gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null},Cu(t)){if(e)throw Error(s(479))}else e=Ii(t,l,a,2),e!==null&&ce(e,t,2)}function Cu(t){var e=t.alternate;return t===P||e!==null&&e===P}function vo(t,e){Ra=Eu=!0;var l=t.pending;l===null?e.next=e:(e.next=l.next,l.next=e),t.pending=e}function go(t,e,l){if((l&4194048)!==0){var a=e.lanes;a&=t.pendingLanes,l|=a,e.lanes=l,Es(t,l)}}var dn={readContext:Vt,use:Ou,useCallback:Mt,useContext:Mt,useEffect:Mt,useImperativeHandle:Mt,useLayoutEffect:Mt,useInsertionEffect:Mt,useMemo:Mt,useReducer:Mt,useRef:Mt,useState:Mt,useDebugValue:Mt,useDeferredValue:Mt,useTransition:Mt,useSyncExternalStore:Mt,useId:Mt,useHostTransitionStatus:Mt,useFormState:Mt,useActionState:Mt,useOptimistic:Mt,useMemoCache:Mt,useCacheRefresh:Mt};dn.useEffectEvent=Mt;var po={readContext:Vt,use:Ou,useCallback:function(t,e){return te().memoizedState=[t,e===void 0?null:e],t},useContext:Vt,useEffect:to,useImperativeHandle:function(t,e,l){l=l!=null?l.concat([t]):null,zu(4194308,4,no.bind(null,e,t),l)},useLayoutEffect:function(t,e){return zu(4194308,4,t,e)},useInsertionEffect:function(t,e){zu(4,2,t,e)},useMemo:function(t,e){var l=te();e=e===void 0?null:e;var a=t();if($l){fl(!0);try{t()}finally{fl(!1)}}return l.memoizedState=[a,e],a},useReducer:function(t,e,l){var a=te();if(l!==void 0){var n=l(e);if($l){fl(!0);try{l(e)}finally{fl(!1)}}}else n=e;return a.memoizedState=a.baseState=n,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:n},a.queue=t,t=t.dispatch=Ly.bind(null,P,t),[a.memoizedState,t]},useRef:function(t){var e=te();return t={current:t},e.memoizedState=t},useState:function(t){t=Cc(t);var e=t.queue,l=yo.bind(null,P,e);return e.dispatch=l,[t.memoizedState,l]},useDebugValue:Uc,useDeferredValue:function(t,e){var l=te();return xc(l,t,e)},useTransition:function(){var t=Cc(!1);return t=so.bind(null,P,t.queue,!0,!1),te().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,e,l){var a=P,n=te();if(ft){if(l===void 0)throw Error(s(407));l=l()}else{if(l=e(),Tt===null)throw Error(s(349));(nt&127)!==0||jr(a,e,l)}n.memoizedState=l;var i={value:l,getSnapshot:e};return n.queue=i,to(Yr.bind(null,a,i,t),[t]),a.flags|=2048,Aa(9,{destroy:void 0},Lr.bind(null,a,i,l,e),null),l},useId:function(){var t=te(),e=Tt.identifierPrefix;if(ft){var l=Qe,a=Be;l=(a&~(1<<32-oe(a)-1)).toString(32)+l,e="_"+e+"R_"+l,l=Tu++,0<\/script>",i=i.removeChild(i.firstChild);break;case"select":i=typeof a.is=="string"?f.createElement("select",{is:a.is}):f.createElement("select"),a.multiple?i.multiple=!0:a.size&&(i.size=a.size);break;default:i=typeof a.is=="string"?f.createElement(n,{is:a.is}):f.createElement(n)}}i[wt]=e,i[ee]=a;t:for(f=e.child;f!==null;){if(f.tag===5||f.tag===6)i.appendChild(f.stateNode);else if(f.tag!==4&&f.tag!==27&&f.child!==null){f.child.return=f,f=f.child;continue}if(f===e)break t;for(;f.sibling===null;){if(f.return===null||f.return===e)break t;f=f.return}f.sibling.return=f.return,f=f.sibling}e.stateNode=i;t:switch(Jt(i,n,a),n){case"button":case"input":case"select":case"textarea":a=!!a.autoFocus;break t;case"img":a=!0;break t;default:a=!1}a&&ke(e)}}return Ot(e),$c(e,e.type,t===null?null:t.memoizedProps,e.pendingProps,l),null;case 6:if(t&&e.stateNode!=null)t.memoizedProps!==a&&ke(e);else{if(typeof a!="string"&&e.stateNode===null)throw Error(s(166));if(t=et.current,va(e)){if(t=e.stateNode,l=e.memoizedProps,a=null,n=Zt,n!==null)switch(n.tag){case 27:case 5:a=n.memoizedProps}t[wt]=e,t=!!(t.nodeValue===l||a!==null&&a.suppressHydrationWarning===!0||Qh(t.nodeValue,l)),t||dl(e,!0)}else t=Fu(t).createTextNode(a),t[wt]=e,e.stateNode=t}return Ot(e),null;case 31:if(l=e.memoizedState,t===null||t.memoizedState!==null){if(a=va(e),l!==null){if(t===null){if(!a)throw Error(s(318));if(t=e.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(s(557));t[wt]=e}else Xl(),(e.flags&128)===0&&(e.memoizedState=null),e.flags|=4;Ot(e),t=!1}else l=uc(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=l),t=!0;if(!t)return e.flags&256?(ye(e),e):(ye(e),null);if((e.flags&128)!==0)throw Error(s(558))}return Ot(e),null;case 13:if(a=e.memoizedState,t===null||t.memoizedState!==null&&t.memoizedState.dehydrated!==null){if(n=va(e),a!==null&&a.dehydrated!==null){if(t===null){if(!n)throw Error(s(318));if(n=e.memoizedState,n=n!==null?n.dehydrated:null,!n)throw Error(s(317));n[wt]=e}else Xl(),(e.flags&128)===0&&(e.memoizedState=null),e.flags|=4;Ot(e),n=!1}else n=uc(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=n),n=!0;if(!n)return e.flags&256?(ye(e),e):(ye(e),null)}return ye(e),(e.flags&128)!==0?(e.lanes=l,e):(l=a!==null,t=t!==null&&t.memoizedState!==null,l&&(a=e.child,n=null,a.alternate!==null&&a.alternate.memoizedState!==null&&a.alternate.memoizedState.cachePool!==null&&(n=a.alternate.memoizedState.cachePool.pool),i=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(i=a.memoizedState.cachePool.pool),i!==n&&(a.flags|=2048)),l!==t&&l&&(e.child.flags|=8192),Nu(e,e.updateQueue),Ot(e),null);case 4:return Dt(),t===null&&pf(e.stateNode.containerInfo),Ot(e),null;case 10:return Je(e.type),Ot(e),null;case 19:if(H(Ut),a=e.memoizedState,a===null)return Ot(e),null;if(n=(e.flags&128)!==0,i=a.rendering,i===null)if(n)yn(a,!1);else{if(Ct!==0||t!==null&&(t.flags&128)!==0)for(t=e.child;t!==null;){if(i=bu(t),i!==null){for(e.flags|=128,yn(a,!1),t=i.updateQueue,e.updateQueue=t,Nu(e,t),e.subtreeFlags=0,t=l,l=e.child;l!==null;)mr(l,t),l=l.sibling;return Y(Ut,Ut.current&1|2),ft&&Ve(e,a.treeForkCount),e.child}t=t.sibling}a.tail!==null&&se()>ju&&(e.flags|=128,n=!0,yn(a,!1),e.lanes=4194304)}else{if(!n)if(t=bu(i),t!==null){if(e.flags|=128,n=!0,t=t.updateQueue,e.updateQueue=t,Nu(e,t),yn(a,!0),a.tail===null&&a.tailMode==="hidden"&&!i.alternate&&!ft)return Ot(e),null}else 2*se()-a.renderingStartTime>ju&&l!==536870912&&(e.flags|=128,n=!0,yn(a,!1),e.lanes=4194304);a.isBackwards?(i.sibling=e.child,e.child=i):(t=a.last,t!==null?t.sibling=i:e.child=i,a.last=i)}return a.tail!==null?(t=a.tail,a.rendering=t,a.tail=t.sibling,a.renderingStartTime=se(),t.sibling=null,l=Ut.current,Y(Ut,n?l&1|2:l&1),ft&&Ve(e,a.treeForkCount),t):(Ot(e),null);case 22:case 23:return ye(e),pc(),a=e.memoizedState!==null,t!==null?t.memoizedState!==null!==a&&(e.flags|=8192):a&&(e.flags|=8192),a?(l&536870912)!==0&&(e.flags&128)===0&&(Ot(e),e.subtreeFlags&6&&(e.flags|=8192)):Ot(e),l=e.updateQueue,l!==null&&Nu(e,l.retryQueue),l=null,t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(l=t.memoizedState.cachePool.pool),a=null,e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(a=e.memoizedState.cachePool.pool),a!==l&&(e.flags|=2048),t!==null&&H(Vl),null;case 24:return l=null,t!==null&&(l=t.memoizedState.cache),e.memoizedState.cache!==l&&(e.flags|=2048),Je(Ht),Ot(e),null;case 25:return null;case 30:return null}throw Error(s(156,e.tag))}function Zy(t,e){switch(ac(e),e.tag){case 1:return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 3:return Je(Ht),Dt(),t=e.flags,(t&65536)!==0&&(t&128)===0?(e.flags=t&-65537|128,e):null;case 26:case 27:case 5:return Zn(e),null;case 31:if(e.memoizedState!==null){if(ye(e),e.alternate===null)throw Error(s(340));Xl()}return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 13:if(ye(e),t=e.memoizedState,t!==null&&t.dehydrated!==null){if(e.alternate===null)throw Error(s(340));Xl()}return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 19:return H(Ut),null;case 4:return Dt(),null;case 10:return Je(e.type),null;case 22:case 23:return ye(e),pc(),t!==null&&H(Vl),t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 24:return Je(Ht),null;case 25:return null;default:return null}}function Xo(t,e){switch(ac(e),e.tag){case 3:Je(Ht),Dt();break;case 26:case 27:case 5:Zn(e);break;case 4:Dt();break;case 31:e.memoizedState!==null&&ye(e);break;case 13:ye(e);break;case 19:H(Ut);break;case 10:Je(e.type);break;case 22:case 23:ye(e),pc(),t!==null&&H(Vl);break;case 24:Je(Ht)}}function vn(t,e){try{var l=e.updateQueue,a=l!==null?l.lastEffect:null;if(a!==null){var n=a.next;l=n;do{if((l.tag&t)===t){a=void 0;var i=l.create,f=l.inst;a=i(),f.destroy=a}l=l.next}while(l!==n)}}catch(o){yt(e,e.return,o)}}function bl(t,e,l){try{var a=e.updateQueue,n=a!==null?a.lastEffect:null;if(n!==null){var i=n.next;a=i;do{if((a.tag&t)===t){var f=a.inst,o=f.destroy;if(o!==void 0){f.destroy=void 0,n=e;var m=l,A=o;try{A()}catch(_){yt(n,m,_)}}}a=a.next}while(a!==i)}}catch(_){yt(e,e.return,_)}}function wo(t){var e=t.updateQueue;if(e!==null){var l=t.stateNode;try{xr(e,l)}catch(a){yt(t,t.return,a)}}}function Zo(t,e,l){l.props=Wl(t.type,t.memoizedProps),l.state=t.memoizedState;try{l.componentWillUnmount()}catch(a){yt(t,e,a)}}function gn(t,e){try{var l=t.ref;if(l!==null){switch(t.tag){case 26:case 27:case 5:var a=t.stateNode;break;case 30:a=t.stateNode;break;default:a=t.stateNode}typeof l=="function"?t.refCleanup=l(a):l.current=a}}catch(n){yt(t,e,n)}}function je(t,e){var l=t.ref,a=t.refCleanup;if(l!==null)if(typeof a=="function")try{a()}catch(n){yt(t,e,n)}finally{t.refCleanup=null,t=t.alternate,t!=null&&(t.refCleanup=null)}else if(typeof l=="function")try{l(null)}catch(n){yt(t,e,n)}else l.current=null}function Vo(t){var e=t.type,l=t.memoizedProps,a=t.stateNode;try{t:switch(e){case"button":case"input":case"select":case"textarea":l.autoFocus&&a.focus();break t;case"img":l.src?a.src=l.src:l.srcSet&&(a.srcset=l.srcSet)}}catch(n){yt(t,t.return,n)}}function Wc(t,e,l){try{var a=t.stateNode;hv(a,t.type,l,e),a[ee]=e}catch(n){yt(t,t.return,n)}}function Ko(t){return t.tag===5||t.tag===3||t.tag===26||t.tag===27&&Ml(t.type)||t.tag===4}function kc(t){t:for(;;){for(;t.sibling===null;){if(t.return===null||Ko(t.return))return null;t=t.return}for(t.sibling.return=t.return,t=t.sibling;t.tag!==5&&t.tag!==6&&t.tag!==18;){if(t.tag===27&&Ml(t.type)||t.flags&2||t.child===null||t.tag===4)continue t;t.child.return=t,t=t.child}if(!(t.flags&2))return t.stateNode}}function Ic(t,e,l){var a=t.tag;if(a===5||a===6)t=t.stateNode,e?(l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l).insertBefore(t,e):(e=l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l,e.appendChild(t),l=l._reactRootContainer,l!=null||e.onclick!==null||(e.onclick=Xe));else if(a!==4&&(a===27&&Ml(t.type)&&(l=t.stateNode,e=null),t=t.child,t!==null))for(Ic(t,e,l),t=t.sibling;t!==null;)Ic(t,e,l),t=t.sibling}function Hu(t,e,l){var a=t.tag;if(a===5||a===6)t=t.stateNode,e?l.insertBefore(t,e):l.appendChild(t);else if(a!==4&&(a===27&&Ml(t.type)&&(l=t.stateNode),t=t.child,t!==null))for(Hu(t,e,l),t=t.sibling;t!==null;)Hu(t,e,l),t=t.sibling}function Jo(t){var e=t.stateNode,l=t.memoizedProps;try{for(var a=t.type,n=e.attributes;n.length;)e.removeAttributeNode(n[0]);Jt(e,a,l),e[wt]=t,e[ee]=l}catch(i){yt(t,t.return,i)}}var Ie=!1,Qt=!1,Pc=!1,Fo=typeof WeakSet=="function"?WeakSet:Set,Xt=null;function Vy(t,e){if(t=t.containerInfo,Ef=ei,t=ur(t),Ki(t)){if("selectionStart"in t)var l={start:t.selectionStart,end:t.selectionEnd};else t:{l=(l=t.ownerDocument)&&l.defaultView||window;var a=l.getSelection&&l.getSelection();if(a&&a.rangeCount!==0){l=a.anchorNode;var n=a.anchorOffset,i=a.focusNode;a=a.focusOffset;try{l.nodeType,i.nodeType}catch{l=null;break t}var f=0,o=-1,m=-1,A=0,_=0,x=t,z=null;e:for(;;){for(var M;x!==l||n!==0&&x.nodeType!==3||(o=f+n),x!==i||a!==0&&x.nodeType!==3||(m=f+a),x.nodeType===3&&(f+=x.nodeValue.length),(M=x.firstChild)!==null;)z=x,x=M;for(;;){if(x===t)break e;if(z===l&&++A===n&&(o=f),z===i&&++_===a&&(m=f),(M=x.nextSibling)!==null)break;x=z,z=x.parentNode}x=M}l=o===-1||m===-1?null:{start:o,end:m}}else l=null}l=l||{start:0,end:0}}else l=null;for(Tf={focusedElem:t,selectionRange:l},ei=!1,Xt=e;Xt!==null;)if(e=Xt,t=e.child,(e.subtreeFlags&1028)!==0&&t!==null)t.return=e,Xt=t;else for(;Xt!==null;){switch(e=Xt,i=e.alternate,t=e.flags,e.tag){case 0:if((t&4)!==0&&(t=e.updateQueue,t=t!==null?t.events:null,t!==null))for(l=0;l title"))),Jt(i,a,l),i[wt]=t,Gt(i),a=i;break t;case"link":var f=td("link","href",n).get(a+(l.href||""));if(f){for(var o=0;oSt&&(f=St,St=$,$=f);var b=ar(o,$),p=ar(o,St);if(b&&p&&(M.rangeCount!==1||M.anchorNode!==b.node||M.anchorOffset!==b.offset||M.focusNode!==p.node||M.focusOffset!==p.offset)){var O=x.createRange();O.setStart(b.node,b.offset),M.removeAllRanges(),$>St?(M.addRange(O),M.extend(p.node,p.offset)):(O.setEnd(p.node,p.offset),M.addRange(O))}}}}for(x=[],M=o;M=M.parentNode;)M.nodeType===1&&x.push({element:M,left:M.scrollLeft,top:M.scrollTop});for(typeof o.focus=="function"&&o.focus(),o=0;ol?32:l,D.T=null,l=cf,cf=null;var i=Ol,f=al;if(jt=0,Da=Ol=null,al=0,(ht&6)!==0)throw Error(s(331));var o=ht;if(ht|=4,uh(i.current),lh(i,i.current,f,l),ht=o,Rn(0,!1),re&&typeof re.onPostCommitFiberRoot=="function")try{re.onPostCommitFiberRoot(Ya,i)}catch{}return!0}finally{L.p=n,D.T=a,Rh(t,e)}}function Ah(t,e,l){e=Oe(l,e),e=Lc(t.stateNode,e,2),t=gl(t,e,2),t!==null&&(Xa(t,2),Le(t))}function yt(t,e,l){if(t.tag===3)Ah(t,t,l);else for(;e!==null;){if(e.tag===3){Ah(e,t,l);break}else if(e.tag===1){var a=e.stateNode;if(typeof e.type.getDerivedStateFromError=="function"||typeof a.componentDidCatch=="function"&&(Rl===null||!Rl.has(a))){t=Oe(l,t),l=zo(2),a=gl(e,l,2),a!==null&&(Mo(l,a,e,t),Xa(a,2),Le(a));break}}e=e.return}}function of(t,e,l){var a=t.pingCache;if(a===null){a=t.pingCache=new Fy;var n=new Set;a.set(e,n)}else n=a.get(e),n===void 0&&(n=new Set,a.set(e,n));n.has(l)||(lf=!0,n.add(l),t=Py.bind(null,t,e,l),e.then(t,t))}function Py(t,e,l){var a=t.pingCache;a!==null&&a.delete(e),t.pingedLanes|=t.suspendedLanes&l,t.warmLanes&=~l,Tt===t&&(nt&l)===l&&(Ct===4||Ct===3&&(nt&62914560)===nt&&300>se()-Qu?(ht&2)===0&&Ua(t,0):af|=l,_a===nt&&(_a=0)),Le(t)}function zh(t,e){e===0&&(e=Ss()),t=Yl(t,e),t!==null&&(Xa(t,e),Le(t))}function tv(t){var e=t.memoizedState,l=0;e!==null&&(l=e.retryLane),zh(t,l)}function ev(t,e){var l=0;switch(t.tag){case 31:case 13:var a=t.stateNode,n=t.memoizedState;n!==null&&(l=n.retryLane);break;case 19:a=t.stateNode;break;case 22:a=t.stateNode._retryCache;break;default:throw Error(s(314))}a!==null&&a.delete(e),zh(t,l)}function lv(t,e){return Ri(t,e)}var Zu=null,Na=null,hf=!1,Vu=!1,df=!1,zl=0;function Le(t){t!==Na&&t.next===null&&(Na===null?Zu=Na=t:Na=Na.next=t),Vu=!0,hf||(hf=!0,nv())}function Rn(t,e){if(!df&&Vu){df=!0;do for(var l=!1,a=Zu;a!==null;){if(t!==0){var n=a.pendingLanes;if(n===0)var i=0;else{var f=a.suspendedLanes,o=a.pingedLanes;i=(1<<31-oe(42|t)+1)-1,i&=n&~(f&~o),i=i&201326741?i&201326741|1:i?i|2:0}i!==0&&(l=!0,Dh(a,i))}else i=nt,i=$n(a,a===Tt?i:0,a.cancelPendingCommit!==null||a.timeoutHandle!==-1),(i&3)===0||Ga(a,i)||(l=!0,Dh(a,i));a=a.next}while(l);df=!1}}function av(){Mh()}function Mh(){Vu=hf=!1;var t=0;zl!==0&&mv()&&(t=zl);for(var e=se(),l=null,a=Zu;a!==null;){var n=a.next,i=Ch(a,e);i===0?(a.next=null,l===null?Zu=n:l.next=n,n===null&&(Na=l)):(l=a,(t!==0||(i&3)!==0)&&(Vu=!0)),a=n}jt!==0&&jt!==5||Rn(t),zl!==0&&(zl=0)}function Ch(t,e){for(var l=t.suspendedLanes,a=t.pingedLanes,n=t.expirationTimes,i=t.pendingLanes&-62914561;0o)break;var _=m.transferSize,x=m.initiatorType;_&&jh(x)&&(m=m.responseEnd,f+=_*(m"u"?null:document;function Wh(t,e,l){var a=Ha;if(a&&typeof e=="string"&&e){var n=Te(e);n='link[rel="'+t+'"][href="'+n+'"]',typeof l=="string"&&(n+='[crossorigin="'+l+'"]'),$h.has(n)||($h.add(n),t={rel:t,crossOrigin:l,href:e},a.querySelector(n)===null&&(e=a.createElement("link"),Jt(e,"link",t),Gt(e),a.head.appendChild(e)))}}function Rv(t){nl.D(t),Wh("dns-prefetch",t,null)}function Ov(t,e){nl.C(t,e),Wh("preconnect",t,e)}function Av(t,e,l){nl.L(t,e,l);var a=Ha;if(a&&t&&e){var n='link[rel="preload"][as="'+Te(e)+'"]';e==="image"&&l&&l.imageSrcSet?(n+='[imagesrcset="'+Te(l.imageSrcSet)+'"]',typeof l.imageSizes=="string"&&(n+='[imagesizes="'+Te(l.imageSizes)+'"]')):n+='[href="'+Te(t)+'"]';var i=n;switch(e){case"style":i=qa(t);break;case"script":i=Ba(t)}De.has(i)||(t=R({rel:"preload",href:e==="image"&&l&&l.imageSrcSet?void 0:t,as:e},l),De.set(i,t),a.querySelector(n)!==null||e==="style"&&a.querySelector(Mn(i))||e==="script"&&a.querySelector(Cn(i))||(e=a.createElement("link"),Jt(e,"link",t),Gt(e),a.head.appendChild(e)))}}function zv(t,e){nl.m(t,e);var l=Ha;if(l&&t){var a=e&&typeof e.as=="string"?e.as:"script",n='link[rel="modulepreload"][as="'+Te(a)+'"][href="'+Te(t)+'"]',i=n;switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":i=Ba(t)}if(!De.has(i)&&(t=R({rel:"modulepreload",href:t},e),De.set(i,t),l.querySelector(n)===null)){switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(l.querySelector(Cn(i)))return}a=l.createElement("link"),Jt(a,"link",t),Gt(a),l.head.appendChild(a)}}}function Mv(t,e,l){nl.S(t,e,l);var a=Ha;if(a&&t){var n=aa(a).hoistableStyles,i=qa(t);e=e||"default";var f=n.get(i);if(!f){var o={loading:0,preload:null};if(f=a.querySelector(Mn(i)))o.loading=5;else{t=R({rel:"stylesheet",href:t,"data-precedence":e},l),(l=De.get(i))&&_f(t,l);var m=f=a.createElement("link");Gt(m),Jt(m,"link",t),m._p=new Promise(function(A,_){m.onload=A,m.onerror=_}),m.addEventListener("load",function(){o.loading|=1}),m.addEventListener("error",function(){o.loading|=2}),o.loading|=4,Wu(f,e,a)}f={type:"stylesheet",instance:f,count:1,state:o},n.set(i,f)}}}function Cv(t,e){nl.X(t,e);var l=Ha;if(l&&t){var a=aa(l).hoistableScripts,n=Ba(t),i=a.get(n);i||(i=l.querySelector(Cn(n)),i||(t=R({src:t,async:!0},e),(e=De.get(n))&&Df(t,e),i=l.createElement("script"),Gt(i),Jt(i,"link",t),l.head.appendChild(i)),i={type:"script",instance:i,count:1,state:null},a.set(n,i))}}function _v(t,e){nl.M(t,e);var l=Ha;if(l&&t){var a=aa(l).hoistableScripts,n=Ba(t),i=a.get(n);i||(i=l.querySelector(Cn(n)),i||(t=R({src:t,async:!0,type:"module"},e),(e=De.get(n))&&Df(t,e),i=l.createElement("script"),Gt(i),Jt(i,"link",t),l.head.appendChild(i)),i={type:"script",instance:i,count:1,state:null},a.set(n,i))}}function kh(t,e,l,a){var n=(n=et.current)?$u(n):null;if(!n)throw Error(s(446));switch(t){case"meta":case"title":return null;case"style":return typeof l.precedence=="string"&&typeof l.href=="string"?(e=qa(l.href),l=aa(n).hoistableStyles,a=l.get(e),a||(a={type:"style",instance:null,count:0,state:null},l.set(e,a)),a):{type:"void",instance:null,count:0,state:null};case"link":if(l.rel==="stylesheet"&&typeof l.href=="string"&&typeof l.precedence=="string"){t=qa(l.href);var i=aa(n).hoistableStyles,f=i.get(t);if(f||(n=n.ownerDocument||n,f={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},i.set(t,f),(i=n.querySelector(Mn(t)))&&!i._p&&(f.instance=i,f.state.loading=5),De.has(t)||(l={rel:"preload",as:"style",href:l.href,crossOrigin:l.crossOrigin,integrity:l.integrity,media:l.media,hrefLang:l.hrefLang,referrerPolicy:l.referrerPolicy},De.set(t,l),i||Dv(n,t,l,f.state))),e&&a===null)throw Error(s(528,""));return f}if(e&&a!==null)throw Error(s(529,""));return null;case"script":return e=l.async,l=l.src,typeof l=="string"&&e&&typeof e!="function"&&typeof e!="symbol"?(e=Ba(l),l=aa(n).hoistableScripts,a=l.get(e),a||(a={type:"script",instance:null,count:0,state:null},l.set(e,a)),a):{type:"void",instance:null,count:0,state:null};default:throw Error(s(444,t))}}function qa(t){return'href="'+Te(t)+'"'}function Mn(t){return'link[rel="stylesheet"]['+t+"]"}function Ih(t){return R({},t,{"data-precedence":t.precedence,precedence:null})}function Dv(t,e,l,a){t.querySelector('link[rel="preload"][as="style"]['+e+"]")?a.loading=1:(e=t.createElement("link"),a.preload=e,e.addEventListener("load",function(){return a.loading|=1}),e.addEventListener("error",function(){return a.loading|=2}),Jt(e,"link",l),Gt(e),t.head.appendChild(e))}function Ba(t){return'[src="'+Te(t)+'"]'}function Cn(t){return"script[async]"+t}function Ph(t,e,l){if(e.count++,e.instance===null)switch(e.type){case"style":var a=t.querySelector('style[data-href~="'+Te(l.href)+'"]');if(a)return e.instance=a,Gt(a),a;var n=R({},l,{"data-href":l.href,"data-precedence":l.precedence,href:null,precedence:null});return a=(t.ownerDocument||t).createElement("style"),Gt(a),Jt(a,"style",n),Wu(a,l.precedence,t),e.instance=a;case"stylesheet":n=qa(l.href);var i=t.querySelector(Mn(n));if(i)return e.state.loading|=4,e.instance=i,Gt(i),i;a=Ih(l),(n=De.get(n))&&_f(a,n),i=(t.ownerDocument||t).createElement("link"),Gt(i);var f=i;return f._p=new Promise(function(o,m){f.onload=o,f.onerror=m}),Jt(i,"link",a),e.state.loading|=4,Wu(i,l.precedence,t),e.instance=i;case"script":return i=Ba(l.src),(n=t.querySelector(Cn(i)))?(e.instance=n,Gt(n),n):(a=l,(n=De.get(i))&&(a=R({},l),Df(a,n)),t=t.ownerDocument||t,n=t.createElement("script"),Gt(n),Jt(n,"link",a),t.head.appendChild(n),e.instance=n);case"void":return null;default:throw Error(s(443,e.type))}else e.type==="stylesheet"&&(e.state.loading&4)===0&&(a=e.instance,e.state.loading|=4,Wu(a,l.precedence,t));return e.instance}function Wu(t,e,l){for(var a=l.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),n=a.length?a[a.length-1]:null,i=n,f=0;f title"):null)}function Uv(t,e,l){if(l===1||e.itemProp!=null)return!1;switch(t){case"meta":case"title":return!0;case"style":if(typeof e.precedence!="string"||typeof e.href!="string"||e.href==="")break;return!0;case"link":if(typeof e.rel!="string"||typeof e.href!="string"||e.href===""||e.onLoad||e.onError)break;return e.rel==="stylesheet"?(t=e.disabled,typeof e.precedence=="string"&&t==null):!0;case"script":if(e.async&&typeof e.async!="function"&&typeof e.async!="symbol"&&!e.onLoad&&!e.onError&&e.src&&typeof e.src=="string")return!0}return!1}function ld(t){return!(t.type==="stylesheet"&&(t.state.loading&3)===0)}function xv(t,e,l,a){if(l.type==="stylesheet"&&(typeof a.media!="string"||matchMedia(a.media).matches!==!1)&&(l.state.loading&4)===0){if(l.instance===null){var n=qa(a.href),i=e.querySelector(Mn(n));if(i){e=i._p,e!==null&&typeof e=="object"&&typeof e.then=="function"&&(t.count++,t=Iu.bind(t),e.then(t,t)),l.state.loading|=4,l.instance=i,Gt(i);return}i=e.ownerDocument||e,a=Ih(a),(n=De.get(n))&&_f(a,n),i=i.createElement("link"),Gt(i);var f=i;f._p=new Promise(function(o,m){f.onload=o,f.onerror=m}),Jt(i,"link",a),l.instance=i}t.stylesheets===null&&(t.stylesheets=new Map),t.stylesheets.set(l,e),(e=l.state.preload)&&(l.state.loading&3)===0&&(t.count++,l=Iu.bind(t),e.addEventListener("load",l),e.addEventListener("error",l))}}var Uf=0;function Nv(t,e){return t.stylesheets&&t.count===0&&ti(t,t.stylesheets),0Uf?50:800)+e);return t.unsuspend=l,function(){t.unsuspend=null,clearTimeout(a),clearTimeout(n)}}:null}function Iu(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)ti(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var Pu=null;function ti(t,e){t.stylesheets=null,t.unsuspend!==null&&(t.count++,Pu=new Map,e.forEach(Hv,t),Pu=null,Iu.call(t))}function Hv(t,e){if(!(e.state.loading&4)){var l=Pu.get(t);if(l)var a=l.get(null);else{l=new Map,Pu.set(t,l);for(var n=t.querySelectorAll("link[data-precedence],style[data-precedence]"),i=0;i"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(u)}catch(c){console.error(c)}}return u(),Yf.exports=Wv(),Yf.exports}var Iv=kv();var ls=/^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i,wd=/^[\\/]{2}/;function Pv(u,c){return c+u.replace(/\\/g,"/")}var zd="popstate";function Md(u){return typeof u=="object"&&u!=null&&"pathname"in u&&"search"in u&&"hash"in u&&"state"in u&&"key"in u}function t0(u={}){function c(s,h){let d=h.state?.masked,{pathname:v,search:E,hash:y}=d||s.location;return Jf("",{pathname:v,search:E,hash:y},h.state&&h.state.usr||null,h.state&&h.state.key||"default",d?{pathname:s.location.pathname,search:s.location.search,hash:s.location.hash}:void 0)}function r(s,h){return typeof h=="string"?h:Bn(h)}return l0(c,r,null,u)}function Yt(u,c){if(u===!1||u===null||typeof u>"u")throw new Error(c)}function Ye(u,c){if(!u){typeof console<"u"&&console.warn(c);try{throw new Error(c)}catch{}}}function e0(){return Math.random().toString(36).substring(2,10)}function Cd(u,c){return{usr:u.state,key:u.key,idx:c,masked:u.mask?{pathname:u.pathname,search:u.search,hash:u.hash}:void 0}}function Jf(u,c,r=null,s,h){return{pathname:typeof u=="string"?u:u.pathname,search:"",hash:"",...typeof c=="string"?Yn(c):c,state:r,key:c&&c.key||s||e0(),mask:h}}function Bn({pathname:u="/",search:c="",hash:r=""}){return c&&c!=="?"&&(u+=c.charAt(0)==="?"?c:"?"+c),r&&r!=="#"&&(u+=r.charAt(0)==="#"?r:"#"+r),u}function Yn(u){let c={};if(u){let r=u.indexOf("#");r>=0&&(c.hash=u.substring(r),u=u.substring(0,r));let s=u.indexOf("?");s>=0&&(c.search=u.substring(s),u=u.substring(0,s)),u&&(c.pathname=u)}return c}function l0(u,c,r,s={}){let{window:h=document.defaultView,v5Compat:d=!1}=s,v=h.history,E="POP",y=null,g=T();g==null&&(g=0,v.replaceState({...v.state,idx:g},""));function T(){return(v.state||{idx:null}).idx}function R(){E="POP";let q=T(),X=q==null?null:q-g;g=q,y&&y({action:E,location:B.location,delta:X})}function N(q,X){E="PUSH";let J=Md(q)?q:Jf(B.location,q,X);g=T()+1;let V=Cd(J,g),it=B.createHref(J.mask||J);try{v.pushState(V,"",it)}catch(ct){if(ct instanceof DOMException&&ct.name==="DataCloneError")throw ct;h.location.assign(it)}d&&y&&y({action:E,location:B.location,delta:1})}function j(q,X){E="REPLACE";let J=Md(q)?q:Jf(B.location,q,X);g=T();let V=Cd(J,g),it=B.createHref(J.mask||J);v.replaceState(V,"",it),d&&y&&y({action:E,location:B.location,delta:0})}function Q(q){return a0(h,q)}let B={get action(){return E},get location(){return u(h,v)},listen(q){if(y)throw new Error("A history only accepts one active listener");return h.addEventListener(zd,R),y=q,()=>{h.removeEventListener(zd,R),y=null}},createHref(q){return c(h,q)},createURL:Q,encodeLocation(q){let X=Q(q);return{pathname:X.pathname,search:X.search,hash:X.hash}},push:N,replace:j,go(q){return v.go(q)}};return B}function a0(u,c,r=!1){let s="http://localhost";u&&(s=u.location.origin!=="null"?u.location.origin:u.location.href),Yt(s,"No window.location.(origin|href) available to create URL");let h=typeof c=="string"?c:Bn(c);return h=h.replace(/ $/,"%20"),!r&&wd.test(h)&&(h=s+h),new URL(h,s)}function Zd(u,c,r="/"){return n0(u,c,r,!1)}function n0(u,c,r,s,h){let d=typeof c=="string"?Yn(c):c,v=ul(d.pathname||"/",r);if(v==null)return null;let E=u0(u),y=null,g=v0(v);for(let T=0;y==null&&T{let T={relativePath:g===void 0?v.path||"":g,caseSensitive:v.caseSensitive===!0,childrenIndex:E,route:v};if(T.relativePath.startsWith("/")){if(!T.relativePath.startsWith(s)&&y)return;Yt(T.relativePath.startsWith(s),`Absolute route path "${T.relativePath}" nested under path "${s}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),T.relativePath=T.relativePath.slice(s.length)}let R=qe([s,T.relativePath]),N=r.concat(T);v.children&&v.children.length>0&&(Yt(v.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${R}".`),Vd(v.children,c,N,R,y)),!(v.path==null&&!v.index)&&c.push({path:R,score:d0(R,v.index),routesMeta:N.map((j,Q)=>{let[B,q]=Fd(j.relativePath,j.caseSensitive,Q===N.length-1);return{...j,matcher:B,compiledParams:q}})})};return u.forEach((v,E)=>{if(v.path===""||!v.path?.includes("?"))d(v,E);else for(let y of Kd(v.path))d(v,E,!0,y)}),c}function Kd(u){let c=u.split("/");if(c.length===0)return[];let[r,...s]=c,h=r.endsWith("?"),d=r.replace(/\?$/,"");if(s.length===0)return h?[d,""]:[d];let v=Kd(s.join("/")),E=[];return E.push(...v.map(y=>y===""?d:[d,y].join("/"))),h&&E.push(...v),E.map(y=>u.startsWith("/")&&y===""?"/":y)}function i0(u){u.sort((c,r)=>c.score!==r.score?r.score-c.score:m0(c.routesMeta.map(s=>s.childrenIndex),r.routesMeta.map(s=>s.childrenIndex)))}var c0=/^:[\w-]+$/,f0=3,s0=2,r0=1,o0=10,h0=-2,_d=u=>u==="*";function d0(u,c){let r=u.split("/"),s=r.length;return r.some(_d)&&(s+=h0),c&&(s+=s0),r.filter(h=>!_d(h)).reduce((h,d)=>h+(c0.test(d)?f0:d===""?r0:o0),s)}function m0(u,c){return u.length===c.length&&u.slice(0,-1).every((s,h)=>s===c[h])?u[u.length-1]-c[c.length-1]:0}function y0(u,c,r=!1){let{routesMeta:s}=u,h={},d="/",v=[];for(let E=0;E{if(T==="*"){let Q=E[N]||"";v=d.slice(0,d.length-Q.length).replace(/(.)\/+$/,"$1")}const j=E[N];return R&&!j?g[T]=void 0:g[T]=(j||"").replace(/%2F/g,"/"),g},{}),pathname:d,pathnameBase:v,pattern:u}}function Fd(u,c=!1,r=!0){Ye(u==="*"||!u.endsWith("*")||u.endsWith("/*"),`Route path "${u}" will be treated as if it were "${u.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${u.replace(/\*$/,"/*")}".`);let s=[],h="^"+u.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(v,E,y,g,T)=>{if(s.push({paramName:E,isOptional:y!=null}),y){let R=T.charAt(g+v.length);return R&&R!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return u.endsWith("*")?(s.push({paramName:"*"}),h+=u==="*"||u==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):r?h+="\\/*$":u!==""&&u!=="/"&&(h+="(?:(?=\\/|$))"),[new RegExp(h,c?void 0:"i"),s]}function v0(u){try{return u.split("/").map(c=>decodeURIComponent(c).replace(/\//g,"%2F")).join("/")}catch(c){return Ye(!1,`The URL path "${u}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${c}).`),u}}function ul(u,c){if(c==="/")return u;if(!u.toLowerCase().startsWith(c.toLowerCase()))return null;let r=c.endsWith("/")?c.length-1:c.length,s=u.charAt(r);return s&&s!=="/"?null:u.slice(r)||"/"}function g0(u,c="/"){let{pathname:r,search:s="",hash:h=""}=typeof u=="string"?Yn(u):u,d;return r?(r=Wd(r),r.startsWith("/")?d=Dd(r.substring(1),"/"):d=Dd(r,c)):d=c,{pathname:d,search:b0(s),hash:E0(h)}}function Dd(u,c){let r=di(c).split("/");return u.split("/").forEach(h=>{h===".."?r.length>1&&r.pop():h!=="."&&r.push(h)}),r.length>1?r.join("/"):"/"}function Zf(u,c,r,s){return`Cannot include a '${u}' character in a manually specified \`to.${c}\` field [${JSON.stringify(s)}]. Please separate it out to the \`to.${r}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function p0(u){return u.filter((c,r)=>r===0||c.route.path&&c.route.path.length>0)}function $d(u){let c=p0(u);return c.map((r,s)=>s===c.length-1?r.pathname:r.pathnameBase)}function as(u,c,r,s=!1){let h;typeof u=="string"?h=Yn(u):(h={...u},Yt(!h.pathname||!h.pathname.includes("?"),Zf("?","pathname","search",h)),Yt(!h.pathname||!h.pathname.includes("#"),Zf("#","pathname","hash",h)),Yt(!h.search||!h.search.includes("#"),Zf("#","search","hash",h)));let d=u===""||h.pathname==="",v=d?"/":h.pathname,E;if(v==null)E=r;else{let R=c.length-1;if(!s&&v.startsWith("..")){let N=v.split("/");for(;N[0]==="..";)N.shift(),R-=1;h.pathname=N.join("/")}E=R>=0?c[R]:"/"}let y=g0(h,E),g=v&&v!=="/"&&v.endsWith("/"),T=(d||v===".")&&r.endsWith("/");return!y.pathname.endsWith("/")&&(g||T)&&(y.pathname+="/"),y}var Wd=u=>u.replace(/[\\/]{2,}/g,"/"),qe=u=>Wd(u.join("/")),di=u=>u.replace(/\/+$/,""),S0=u=>di(u).replace(/^\/*/,"/"),b0=u=>!u||u==="?"?"":u.startsWith("?")?u:"?"+u,E0=u=>!u||u==="#"?"":u.startsWith("#")?u:"#"+u,T0=class{constructor(u,c,r,s=!1){this.status=u,this.statusText=c||"",this.internal=s,r instanceof Error?(this.data=r.toString(),this.error=r):this.data=r}};function R0(u){return u!=null&&typeof u.status=="number"&&typeof u.statusText=="string"&&typeof u.internal=="boolean"&&"data"in u}function O0(u){let c=u.map(r=>r.route.path).filter(Boolean);return qe(c)||"/"}var kd=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Id(u,c){let r=u;if(typeof r!="string"||!ls.test(r))return{absoluteURL:void 0,isExternal:!1,to:r};let s=r,h=!1;if(kd)try{let d=new URL(window.location.href),v=wd.test(r)?new URL(Pv(r,d.protocol)):new URL(r),E=ul(v.pathname,c);v.origin===d.origin&&E!=null?r=E+v.search+v.hash:h=!0}catch{Ye(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:s,isExternal:h,to:r}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var Pd=["POST","PUT","PATCH","DELETE"];new Set(Pd);var A0=["GET",...Pd];new Set(A0);var z0=["about:","blob:","chrome:","chrome-untrusted:","content:","data:","devtools:","file:","filesystem:","javascript:"];function M0(u){try{return z0.includes(new URL(u).protocol)}catch{return!1}}var ja=C.createContext(null);ja.displayName="DataRouter";var yi=C.createContext(null);yi.displayName="DataRouterState";var tm=C.createContext(!1);function C0(){return C.useContext(tm)}var em=C.createContext({isTransitioning:!1});em.displayName="ViewTransition";var _0=C.createContext(new Map);_0.displayName="Fetchers";var D0=C.createContext(null);D0.displayName="Await";var Ue=C.createContext(null);Ue.displayName="Navigation";var vi=C.createContext(null);vi.displayName="Location";var il=C.createContext({outlet:null,matches:[],isDataRoute:!1});il.displayName="Route";var ns=C.createContext(null);ns.displayName="RouteError";var lm="REACT_ROUTER_ERROR",U0="REDIRECT",x0="ROUTE_ERROR_RESPONSE";function N0(u){if(u.startsWith(`${lm}:${U0}:{`))try{let c=JSON.parse(u.slice(28));if(typeof c=="object"&&c&&typeof c.status=="number"&&typeof c.statusText=="string"&&typeof c.location=="string"&&typeof c.reloadDocument=="boolean"&&typeof c.replace=="boolean")return c}catch{}}function H0(u){if(u.startsWith(`${lm}:${x0}:{`))try{let c=JSON.parse(u.slice(40));if(typeof c=="object"&&c&&typeof c.status=="number"&&typeof c.statusText=="string")return new T0(c.status,c.statusText,c.data)}catch{}}function q0(u,{relative:c}={}){Yt(Gn(),"useHref() may be used only in the context of a component.");let{basename:r,navigator:s}=C.useContext(Ue),{hash:h,pathname:d,search:v}=Xn(u,{relative:c}),E=d;return r!=="/"&&(E=d==="/"?r:qe([r,d])),s.createHref({pathname:E,search:v,hash:h})}function Gn(){return C.useContext(vi)!=null}function cl(){return Yt(Gn(),"useLocation() may be used only in the context of a component."),C.useContext(vi).location}var am="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function nm(u){C.useContext(Ue).static||C.useLayoutEffect(u)}function B0(){let{isDataRoute:u}=C.useContext(il);return u?$0():Q0()}function Q0(){Yt(Gn(),"useNavigate() may be used only in the context of a component.");let u=C.useContext(ja),{basename:c,navigator:r}=C.useContext(Ue),{matches:s}=C.useContext(il),{pathname:h}=cl(),d=JSON.stringify($d(s)),v=C.useRef(!1);return nm(()=>{v.current=!0}),C.useCallback((y,g={})=>{if(Ye(v.current,am),!v.current)return;if(typeof y=="number"){r.go(y);return}let T=as(y,JSON.parse(d),h,g.relative==="path");u==null&&c!=="/"&&(T.pathname=T.pathname==="/"?c:qe([c,T.pathname])),(g.replace?r.replace:r.push)(T,g.state,g)},[c,r,d,h,u])}C.createContext(null);function Xn(u,{relative:c}={}){let{matches:r}=C.useContext(il),{pathname:s}=cl(),h=JSON.stringify($d(r));return C.useMemo(()=>as(u,JSON.parse(h),s,c==="path"),[u,h,s,c])}function j0(u,c,r){Yt(Gn(),"useRoutes() may be used only in the context of a component.");let{navigator:s}=C.useContext(Ue),{matches:h}=C.useContext(il),d=h[h.length-1],v=d?d.params:{},E=d?d.pathname:"/",y=d?d.pathnameBase:"/",g=d&&d.route;{let q=g&&g.path||"";im(E,!g||q.endsWith("*")||q.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${E}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let T=cl(),R;R=T;let N=R.pathname||"/",j=N;if(y!=="/"){let q=y.replace(/^\//,"").split("/");j="/"+N.replace(/^\//,"").split("/").slice(q.length).join("/")}let Q=r&&r.state.matches.length?r.state.matches.map(q=>Object.assign(q,{route:r.manifest[q.route.id]||q.route})):Zd(u,{pathname:j});return Ye(g||Q!=null,`No routes matched location "${R.pathname}${R.search}${R.hash}" `),Ye(Q==null||Q[Q.length-1].route.element!==void 0||Q[Q.length-1].route.Component!==void 0||Q[Q.length-1].route.lazy!==void 0,`Matched leaf route at location "${R.pathname}${R.search}${R.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`),w0(Q&&Q.map(q=>Object.assign({},q,{params:Object.assign({},v,q.params),pathname:qe([y,s.encodeLocation?s.encodeLocation(q.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:q.pathname]),pathnameBase:q.pathnameBase==="/"?y:qe([y,s.encodeLocation?s.encodeLocation(q.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:q.pathnameBase])})),h,r)}function L0(){let u=F0(),c=R0(u)?`${u.status} ${u.statusText}`:u instanceof Error?u.message:JSON.stringify(u),r=u instanceof Error?u.stack:null,s="rgba(200,200,200, 0.5)",h={padding:"0.5rem",backgroundColor:s},d={padding:"2px 4px",backgroundColor:s},v=null;return console.error("Error handled by React Router default ErrorBoundary:",u),v=C.createElement(C.Fragment,null,C.createElement("p",null,"💿 Hey developer 👋"),C.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",C.createElement("code",{style:d},"ErrorBoundary")," or"," ",C.createElement("code",{style:d},"errorElement")," prop on your route.")),C.createElement(C.Fragment,null,C.createElement("h2",null,"Unexpected Application Error!"),C.createElement("h3",{style:{fontStyle:"italic"}},c),r?C.createElement("pre",{style:h},r):null,v)}var Y0=C.createElement(L0,null),um=class extends C.Component{constructor(u){super(u),this.state={location:u.location,revalidation:u.revalidation,error:u.error}}static getDerivedStateFromError(u){return{error:u}}static getDerivedStateFromProps(u,c){return c.location!==u.location||c.revalidation!=="idle"&&u.revalidation==="idle"?{error:u.error,location:u.location,revalidation:u.revalidation}:{error:u.error!==void 0?u.error:c.error,location:c.location,revalidation:u.revalidation||c.revalidation}}componentDidCatch(u,c){this.props.onError?this.props.onError(u,c):console.error("React Router caught the following error during render",u)}render(){let u=this.state.error;if(this.context&&typeof u=="object"&&u&&"digest"in u&&typeof u.digest=="string"){const r=H0(u.digest);r&&(u=r)}let c=u!==void 0?C.createElement(il.Provider,{value:this.props.routeContext},C.createElement(ns.Provider,{value:u,children:this.props.component})):this.props.children;return this.context?C.createElement(G0,{error:u},c):c}};um.contextType=tm;var Vf=new WeakMap;function G0({children:u,error:c}){let{basename:r}=C.useContext(Ue);if(typeof c=="object"&&c&&"digest"in c&&typeof c.digest=="string"){let s=N0(c.digest);if(s){let h=Vf.get(c);if(h)throw h;let d=Id(s.location,r),v=d.absoluteURL||d.to;if(M0(v))throw new Error("Invalid redirect location");if(kd&&!Vf.get(c))if(d.isExternal||s.reloadDocument)window.location.href=v;else{const E=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(d.to,{replace:s.replace}));throw Vf.set(c,E),E}return C.createElement("meta",{httpEquiv:"refresh",content:`0;url=${v}`})}}return u}function X0({routeContext:u,match:c,children:r}){let s=C.useContext(ja);return s&&s.static&&s.staticContext&&(c.route.errorElement||c.route.ErrorBoundary)&&(s.staticContext._deepestRenderedBoundaryId=c.route.id),C.createElement(il.Provider,{value:u},r)}function w0(u,c=[],r){let s=r?.state;if(u==null){if(!s)return null;if(s.errors)u=s.matches;else if(c.length===0&&!s.initialized&&s.matches.length>0)u=s.matches;else return null}let h=u,d=s?.errors;if(d!=null){let T=h.findIndex(R=>R.route.id&&d?.[R.route.id]!==void 0);Yt(T>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(d).join(",")}`),h=h.slice(0,Math.min(h.length,T+1))}let v=!1,E=-1;if(r&&s){v=s.renderFallback;for(let T=0;T=0?h=h.slice(0,E+1):h=[h[0]];break}}}}let y=r?.onError,g=s&&y?(T,R)=>{y(T,{location:s.location,params:s.matches?.[0]?.params??{},pattern:O0(s.matches),errorInfo:R})}:void 0;return h.reduceRight((T,R,N)=>{let j,Q=!1,B=null,q=null;s&&(j=d&&R.route.id?d[R.route.id]:void 0,B=R.route.errorElement||Y0,v&&(E<0&&N===0?(im("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),Q=!0,q=null):E===N&&(Q=!0,q=R.route.hydrateFallbackElement||null)));let X=c.concat(h.slice(0,N+1)),J=()=>{let V;return j?V=B:Q?V=q:R.route.Component?V=C.createElement(R.route.Component,null):R.route.element?V=R.route.element:V=T,C.createElement(X0,{match:R,routeContext:{outlet:T,matches:X,isDataRoute:s!=null},children:V})};return s&&(R.route.ErrorBoundary||R.route.errorElement||N===0)?C.createElement(um,{location:s.location,revalidation:s.revalidation,component:B,error:j,children:J(),routeContext:{outlet:null,matches:X,isDataRoute:!0},onError:g}):J()},null)}function us(u){return`${u} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Z0(u){let c=C.useContext(ja);return Yt(c,us(u)),c}function V0(u){let c=C.useContext(yi);return Yt(c,us(u)),c}function K0(u){let c=C.useContext(il);return Yt(c,us(u)),c}function is(u){let c=K0(u),r=c.matches[c.matches.length-1];return Yt(r.route.id,`${u} can only be used on routes that contain a unique "id"`),r.route.id}function J0(){return is("useRouteId")}function F0(){let u=C.useContext(ns),c=V0("useRouteError"),r=is("useRouteError");return u!==void 0?u:c.errors?.[r]}function $0(){let{router:u}=Z0("useNavigate"),c=is("useNavigate"),r=C.useRef(!1);return nm(()=>{r.current=!0}),C.useCallback(async(h,d={})=>{Ye(r.current,am),r.current&&(typeof h=="number"?await u.navigate(h):await u.navigate(h,{fromRouteId:c,...d}))},[u,c])}var Ud={};function im(u,c,r){!c&&!Ud[u]&&(Ud[u]=!0,Ye(!1,r))}C.memo(W0);function W0({routes:u,manifest:c,future:r,state:s,isStatic:h,onError:d}){return j0(u,void 0,{manifest:c,state:s,isStatic:h,onError:d})}function k0({basename:u="/",children:c=null,location:r,navigationType:s="POP",navigator:h,static:d=!1,useTransitions:v}){Yt(!Gn(),"You cannot render a inside another . You should never have more than one in your app.");let E=u.replace(/^\/*/,"/"),y=C.useMemo(()=>({basename:E,navigator:h,static:d,useTransitions:v,future:{}}),[E,h,d,v]);typeof r=="string"&&(r=Yn(r));let{pathname:g="/",search:T="",hash:R="",state:N=null,key:j="default",mask:Q}=r,B=C.useMemo(()=>{let q=ul(g,E);return q==null?null:{location:{pathname:q,search:T,hash:R,state:N,key:j,mask:Q},navigationType:s}},[E,g,T,R,N,j,s,Q]);return Ye(B!=null,` is not able to match the URL "${g}${T}${R}" because it does not start with the basename, so the won't render anything.`),B==null?null:C.createElement(Ue.Provider,{value:y},C.createElement(vi.Provider,{children:c,value:B}))}var ri="get",oi="application/x-www-form-urlencoded";function gi(u){return typeof HTMLElement<"u"&&u instanceof HTMLElement}function I0(u){return gi(u)&&u.tagName.toLowerCase()==="button"}function P0(u){return gi(u)&&u.tagName.toLowerCase()==="form"}function tg(u){return gi(u)&&u.tagName.toLowerCase()==="input"}function eg(u){return!!(u.metaKey||u.altKey||u.ctrlKey||u.shiftKey)}function lg(u,c){return u.button===0&&(!c||c==="_self")&&!eg(u)}var fi=null;function ag(){if(fi===null)try{new FormData(document.createElement("form"),0),fi=!1}catch{fi=!0}return fi}var ng=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function Kf(u){return u!=null&&!ng.has(u)?(Ye(!1,`"${u}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${oi}"`),null):u}function ug(u,c){let r,s,h,d,v;if(P0(u)){let E=u.getAttribute("action");s=E?ul(E,c):null,r=u.getAttribute("method")||ri,h=Kf(u.getAttribute("enctype"))||oi,d=new FormData(u)}else if(I0(u)||tg(u)&&(u.type==="submit"||u.type==="image")){let E=u.form;if(E==null)throw new Error('Cannot submit a - - -
- - - -
- - -
- -
-
- - - - - - - - - - - -
-
-
Select a file to read it.
-
-
- - +