diff --git a/.cursor/rules/rules.mdc b/.cursor/rules/rules.mdc deleted file mode 100644 index 211df3f..0000000 --- a/.cursor/rules/rules.mdc +++ /dev/null @@ -1,100 +0,0 @@ ---- -description: Golang rules -globs: -alwaysApply: true ---- -You are an expert in Go, microservices architecture, and clean backend development practices. Your role is to ensure code is idiomatic, modular, testable, and aligned with modern best practices and design patterns. - -### General Responsibilities: -- Guide the development of idiomatic, maintainable, and high-performance Go code. -- Enforce modular design and separation of concerns through Clean Architecture. -- Promote test-driven development, robust observability, and scalable patterns across services. - -### Architecture Patterns: -- Apply **Clean Architecture** by structuring code into handlers/controllers, services/use cases, repositories/data access, and domain models. -- Use **domain-driven design** principles where applicable. -- Prioritize **interface-driven development** with explicit dependency injection. -- Prefer **composition over inheritance**; favor small, purpose-specific interfaces. -- Ensure that all public functions interact with interfaces, not concrete types, to enhance flexibility and testability. - -### Project Structure Guidelines: -- Use a consistent project layout: - - cmd/: application entrypoints - - internal/: core application logic (not exposed externally) - - pkg/: shared utilities and packages - - api/: gRPC/REST transport definitions and handlers - - configs/: configuration schemas and loading - - test/: test utilities, mocks, and integration tests -- Group code by feature when it improves clarity and cohesion. -- Keep logic decoupled from framework-specific code. - -### Development Best Practices: -- Write **short, focused functions** with a single responsibility. -- Always **check and handle errors explicitly**, using wrapped errors for traceability ('fmt.Errorf("context: %w", err)'). -- Avoid **global state**; use constructor functions to inject dependencies. -- Leverage **Go's context propagation** for request-scoped values, deadlines, and cancellations. -- Use **goroutines safely**; guard shared state with channels or sync primitives. -- **Defer closing resources** and handle them carefully to avoid leaks. - -### Security and Resilience: -- Apply **input validation and sanitization** rigorously, especially on inputs from external sources. -- Use secure defaults for **JWT, cookies**, and configuration settings. -- Isolate sensitive operations with clear **permission boundaries**. -- Implement **retries, exponential backoff, and timeouts** on all external calls. -- Use **circuit breakers and rate limiting** for service protection. -- Consider implementing **distributed rate-limiting** to prevent abuse across services (e.g., using Redis). - -### Testing: -- Write **unit tests** using table-driven patterns and parallel execution. -- **Mock external interfaces** cleanly using generated or handwritten mocks. -- Separate **fast unit tests** from slower integration and E2E tests. -- Ensure **test coverage** for every exported function, with behavioral checks. -- Use tools like 'go test -cover' to ensure adequate test coverage. - -### Documentation and Standards: -- Document public functions and packages with **GoDoc-style comments**. -- Provide concise **READMEs** for services and libraries. -- Maintain a 'CONTRIBUTING.md' and 'ARCHITECTURE.md' to guide team practices. -- Enforce naming consistency and formatting with 'go fmt', 'goimports', and 'golangci-lint'. - -### Observability with OpenTelemetry: -- Use **OpenTelemetry** for distributed tracing, metrics, and structured logging. -- Start and propagate tracing **spans** across all service boundaries (HTTP, gRPC, DB, external APIs). -- Always attach 'context.Context' to spans, logs, and metric exports. -- Use **otel.Tracer** for creating spans and **otel.Meter** for collecting metrics. -- Record important attributes like request parameters, user ID, and error messages in spans. -- Use **log correlation** by injecting trace IDs into structured logs. -- Export data to **OpenTelemetry Collector**, **Jaeger**, or **Prometheus**. - -### Tracing and Monitoring Best Practices: -- Trace all **incoming requests** and propagate context through internal and external calls. -- Use **middleware** to instrument HTTP and gRPC endpoints automatically. -- Annotate slow, critical, or error-prone paths with **custom spans**. -- Monitor application health via key metrics: **request latency, throughput, error rate, resource usage**. -- Define **SLIs** (e.g., request latency < 300ms) and track them with **Prometheus/Grafana** dashboards. -- Alert on key conditions (e.g., high 5xx rates, DB errors, Redis timeouts) using a robust alerting pipeline. -- Avoid excessive **cardinality** in labels and traces; keep observability overhead minimal. -- Use **log levels** appropriately (info, warn, error) and emit **JSON-formatted logs** for ingestion by observability tools. -- Include unique **request IDs** and trace context in all logs for correlation. - -### Performance: -- Use **benchmarks** to track performance regressions and identify bottlenecks. -- Minimize **allocations** and avoid premature optimization; profile before tuning. -- Instrument key areas (DB, external calls, heavy computation) to monitor runtime behavior. - -### Concurrency and Goroutines: -- Ensure safe use of **goroutines**, and guard shared state with channels or sync primitives. -- Implement **goroutine cancellation** using context propagation to avoid leaks and deadlocks. - -### Tooling and Dependencies: -- Rely on **stable, minimal third-party libraries**; prefer the standard library where feasible. -- Use **Go modules** for dependency management and reproducibility. -- Version-lock dependencies for deterministic builds. -- Integrate **linting, testing, and security checks** in CI pipelines. - -### Key Conventions: -1. Prioritize **readability, simplicity, and maintainability**. -2. Design for **change**: isolate business logic and minimize framework lock-in. -3. Emphasize clear **boundaries** and **dependency inversion**. -4. Ensure all behavior is **observable, testable, and documented**. -5. **Automate workflows** for testing, building, and deployment. \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee7e705..2edc871 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: 'stable' + go-version-file: 'go.mod' - name: Cache Go modules uses: actions/cache@v3 @@ -45,8 +45,8 @@ jobs: - name: Run golangci-lint uses: golangci/golangci-lint-action@v3 with: - version: latest - args: --timeout=5m + version: v1.64.8 + args: --timeout=5m --out-format=colored-line-number test: name: Test @@ -60,7 +60,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: 'stable' + go-version-file: 'go.mod' - name: Cache Go modules uses: actions/cache@v3 diff --git a/.gitignore b/.gitignore index 19faf55..6fcd5b6 100644 --- a/.gitignore +++ b/.gitignore @@ -61,8 +61,11 @@ tmp/ # Docker volumes data/ -# Node modules (if any frontend tooling is added) +# Frontend tooling node_modules/ +web/frontend/dist/* +# Keep the stub so `go build` works before `npm run build` has been run. +!web/frontend/dist/index.html # Backup files *.bak diff --git a/.zed/debug.json b/.zed/debug.json new file mode 100644 index 0000000..90c9cfc --- /dev/null +++ b/.zed/debug.json @@ -0,0 +1,35 @@ +[ + { + "label": "Backend (Go)", + "adapter": "Delve", + "request": "launch", + "mode": "debug", + "program": "./cmd/server", + "cwd": "$ZED_WORKTREE_ROOT", + "env": { + "PORT": "8080", + "DATABASE_PATH": "./golinks.db", + "BASE_URL": "http://localhost:8080", + "ENVIRONMENT": "development" + } + }, + { + "label": "Frontend dev server (Vite)", + "adapter": "JavaScript", + "type": "node", + "request": "launch", + "program": "$ZED_WORKTREE_ROOT/web/frontend/node_modules/vite/bin/vite.js", + "cwd": "$ZED_WORKTREE_ROOT/web/frontend", + "console": "integratedTerminal", + "skipFiles": ["/**"] + }, + { + "label": "Frontend (Chrome)", + "adapter": "JavaScript", + "type": "chrome", + "request": "launch", + "url": "http://localhost:5173", + "webRoot": "$ZED_WORKTREE_ROOT/web/frontend", + "skipFiles": ["/**"] + } +] diff --git a/.zed/tasks.json b/.zed/tasks.json new file mode 100644 index 0000000..cb98530 --- /dev/null +++ b/.zed/tasks.json @@ -0,0 +1,58 @@ +[ + { + "label": "Run backend (Go)", + "command": "go", + "args": ["run", "./cmd/server"], + "cwd": "$ZED_WORKTREE_ROOT", + "env": { + "PORT": "8080", + "DATABASE_PATH": "./golinks.db", + "BASE_URL": "http://localhost:8080", + "ENVIRONMENT": "development" + }, + "use_new_terminal": false, + "allow_concurrent_runs": false, + "reveal": "always", + "hide": "never" + }, + { + "label": "Run frontend dev server (Vite)", + "command": "npm", + "args": ["run", "dev"], + "cwd": "$ZED_WORKTREE_ROOT/web/frontend", + "use_new_terminal": false, + "allow_concurrent_runs": false, + "reveal": "always", + "hide": "never" + }, + { + "label": "Run full stack (make dev)", + "command": "make", + "args": ["dev"], + "cwd": "$ZED_WORKTREE_ROOT", + "use_new_terminal": true, + "allow_concurrent_runs": false, + "reveal": "always", + "hide": "never" + }, + { + "label": "Build frontend", + "command": "npm", + "args": ["run", "build"], + "cwd": "$ZED_WORKTREE_ROOT/web/frontend", + "use_new_terminal": false, + "allow_concurrent_runs": false, + "reveal": "always", + "hide": "on_success" + }, + { + "label": "Build production binary", + "command": "make", + "args": ["build"], + "cwd": "$ZED_WORKTREE_ROOT", + "use_new_terminal": false, + "allow_concurrent_runs": false, + "reveal": "always", + "hide": "on_success" + } +] diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..220635b --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,371 @@ +# Architecture + +This document describes how GoLinks is wired end-to-end: the layered Go backend, the React/Vite SPA, the way they're glued together into a single binary, and exactly what each HTTP endpoint does. + +`README.md` covers what the app is and how to use it. `CLAUDE.md` captures conventions for contributors. This file is the implementation reference — read it when you need to understand or change behaviour. + +--- + +## At a glance + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ golinks (single binary) │ +│ │ +│ cmd/server/main.go │ +│ └── gorilla/mux router │ +│ ├── /query/{path:.*} → 302 redirect │ +│ ├── /api/links (GET/POST) → JSON │ +│ ├── /update/ (POST) → JSON (legacy form) │ +│ ├── /api/docs (GET/POST) → JSON │ +│ ├── /api/docs/{file} (GET/DEL) → JSON │ +│ └── /* (catch-all) → embedded SPA / index.html│ +│ │ +│ internal/ │ +│ handlers ──▶ service ──▶ repository ──▶ database (SQLite) │ +│ ▲ │ │ +│ └── domain models (json + db tagged) ◀────┘ │ +│ │ +│ web/frontend/ │ +│ React 18 + TS + Vite + Tailwind + shadcn/ui │ +│ TanStack Query · react-hook-form+zod · @mdx-js/mdx │ +│ └── dist/ ◀── //go:embed all:dist (web/frontend/embed.go) │ +│ │ +│ docs/ ── on-disk markdown/MDX (read at runtime) │ +│ data/ ── SQLite database file │ +└────────────────────────────────────────────────────────────────────┘ +``` + +One process. Two TCP listeners only in development (`:8080` Go, `:5173` Vite proxying back). In production, one listener: `:8080`. + +--- + +## Repository layout + +``` +cmd/server/main.go Entrypoint: config → DB → repos → services → handlers → router +internal/ +├── config/config.go Env / .env loading; Config struct +├── database/sqlite.go sql.DB + schema migrations +├── domain/models.go Shortcut, Query, KeywordInfo, PopularQuery, LinkRequest (json+db tags) +├── handlers/handler.go Redirect + /api/links + legacy /update/ +├── handlers/document.go /api/docs CRUD +├── handlers/handler_test.go Table-driven tests with mock services +├── logger/logger.go slog wrapper +├── repository/shortcut.go SQL for linktable +├── repository/query.go SQL for queries (analytics) +└── service/ + ├── link.go LinkService: GetLink, UpdateLink, GetRecentQueries, GetAllKeywords + └── document.go DocumentService: GetDocument, SaveDocument, ListDocuments, DeleteDocument +web/frontend/ +├── embed.go //go:embed all:dist + SPA fallback handler +├── vite.config.ts Dev server proxy: /api, /query → :8080 +├── src/ +│ ├── App.tsx Routes +│ ├── main.tsx Entry: QueryClientProvider, BrowserRouter, Toaster +│ ├── index.css Tailwind + Rams tokens → shadcn HSL vars + prose overrides +│ ├── components/ Navbar, LinkForm, KeywordTable, RecentQueries, DocUploader, MDXRenderer +│ ├── components/ui/ shadcn primitives (button, input, card, table, alert, dialog, tabs, sonner, skeleton) +│ ├── pages/ Home, Setup, DocsList, Doc, NotFound +│ └── lib/ +│ ├── api.ts Typed fetch wrappers + ApiError +│ ├── mdx.tsx @mdx-js/mdx evaluate() + component map +│ └── utils.ts cn() helper +docs/ Sample markdown/MDX (and uploaded user docs) +.zed/debug.json Zed debugger configs (Backend Go, Vite, Chrome) +Dockerfile Three-stage: node → go → alpine +Makefile dev / build / test / docker / lint +``` + +--- + +## Backend layers + +The backend follows Clean Architecture. Each layer depends only on the layer below it via interfaces. + +### `cmd/server/main.go` — entrypoint + +Orchestrates startup in this order (`main.go:24-73`): + +1. `config.Load()` — reads `.env` (optional) and env vars (`PORT`, `DATABASE_PATH`, `BASE_URL`, `ENVIRONMENT`, `LOG_LEVEL`). +2. `logger.Initialize(cfg.Logging)` — structured slog logger. +3. `database.NewSQLiteDB(cfg.DatabasePath)` + `database.Migrate(db)` — open connection, run idempotent migrations. +4. Construct repositories (`shortcutRepo`, `queryRepo`). +5. Construct services (`linkService`, `docService`). +6. Construct handlers (`handler`, `docHandler`). +7. Build router; register routes; mount the embedded SPA at the catch-all. +8. Start HTTP server in a goroutine; block on `SIGINT`/`SIGTERM`; graceful shutdown with 30 s timeout. + +### `internal/handlers/` — HTTP transport + +The thinnest layer: parse requests, call services, encode responses. No business logic. Errors of type `service.InvalidQueryError` are mapped to HTTP 400; everything else is 500. + +`writeJSON(w, status, body)` in `internal/handlers/document.go:124` is the canonical JSON encoder used across both files. + +### `internal/service/` — business logic + +The brain. `LinkService` implements golink resolution semantics including space-splitting and aliasing (more under `/query/` below). `DocumentService` does file I/O and frontmatter parsing. + +Services depend on repository **interfaces** (`ShortcutRepository`, `QueryRepository` declared in `service/link.go:15-25`), not concrete types — that's how `mockLinkService` in `handler_test.go` is possible. + +### `internal/repository/` — data access + +Plain `database/sql` queries. No ORM. Each method takes a `context.Context` and returns domain types defined in `internal/domain/`. + +### `internal/database/` — SQLite + +Three tables (`internal/database/sqlite.go:28-46`): + +- **linktable** — `(id, word, link, user, created_at)`. +- **queries** — `(query_id, word_id → linktable.id, created_at)`. +- **tags** — `(id, word_id → linktable.id, tag)` — defined but currently unused. + +Indexes on `linktable.word`, `queries.word_id`, `queries.created_at`. Foreign keys are enabled via the connection string (`?_foreign_keys=on`). + +### `internal/domain/` — shared models + +POGOs (Plain Old Go Objects) with `json:` and `db:` tags. Used unchanged as both DB row targets and API response bodies — so a schema rename ripples through both layers automatically. + +### `web/frontend/embed.go` — SPA bridge + +Compile-time `//go:embed all:dist` pulls the Vite build output into the binary. The exported `Handler(reservedPrefixes...)` returns an `http.Handler` that: + +- Refuses non-GET/HEAD with 405. +- Refuses any path starting with `api/` or `query/` (defense in depth — those routes match earlier, but if registration order ever changes, the SPA must not shadow them). +- Serves real files from the embedded FS when they exist (`/assets/*`, `/favicon.ico`). +- Falls back to `index.html` with `Cache-Control: no-cache` for everything else, so React Router can take over on hard refreshes of `/setup`, `/docs/foo`, etc. + +There's also a `brokenHandler` that returns 503 with a helpful message if the embedded `dist/` is missing `index.html`. Combined with the committed stub `dist/index.html`, this guarantees `git clone && go build` always produces a runnable binary. + +--- + +## Frontend architecture + +### Entry & routing + +`src/main.tsx` mounts `` inside `QueryClientProvider` + `BrowserRouter` and renders `` for sonner. + +`src/App.tsx` is the route table: + +| Path | Component | Notes | +| ---------------- | --------------- | --------------------------------------------- | +| `/` | `HomePage` | Form + keyword list + recent queries | +| `/homepage` | `HomePage` | Legacy alias from the template era | +| `/setup` | `SetupPage` | Per-browser instructions in shadcn `Tabs` | +| `/docs` | `DocsListPage` | List + upload + delete | +| `/docs/:filename`| `DocPage` | Fetches raw source, runtime-compiles MDX | +| `*` | `NotFoundPage` | Custom 404 with shortcuts back to home/setup | + +### State management + +- **Server state → TanStack Query.** Every fetch goes through a `useQuery` (read) or `useMutation` (write). Mutations call `queryClient.invalidateQueries(['links'])` etc. on success to refresh stale views. +- **URL state → `useSearchParams`.** The `?missing=foo` query param after a failed redirect is read in `HomePage` and shown as a toast, then cleared. +- **Form state → `react-hook-form` + `zod`.** `LinkForm` is the canonical example. +- **Local UI state → `useState`.** Component-scoped, never lifted into a global store. + +### API client + +`src/lib/api.ts` exports an `api` object with one function per endpoint. Each is typed end-to-end: + +```ts +api.listLinks() : Promise +api.createLink({word,link}): Promise<{success: true}> +api.listDocs() : Promise<{documents: DocumentInfo[]}> +api.getDoc(filename) : Promise +api.uploadDoc(file: File) : Promise<{success: true; filename; url}> +api.deleteDoc(filename) : Promise<{success: true}> +``` + +Non-2xx responses throw `ApiError` carrying the body text + status code. + +### Styling + +`src/index.css`: + +- Imports `@fontsource/inter` (300/400/500/600), `@fontsource/jetbrains-mono` (400/500), and `highlight.js/styles/github.css`. +- `@tailwind base/components/utilities`. +- Defines shadcn HSL CSS variables under `:root`: `--background`, `--foreground`, `--primary` (Braun orange), `--accent` (functional blue), `--destructive`, `--muted`, etc. These map onto Dieter Rams-inspired tokens — see comments in the file for the original hex values. +- Prose overrides under `@layer base` make MDX-rendered docs match the rest of the UI (orange-on-hover links, mono code blocks, etc.). + +`tailwind.config.ts` extends Tailwind with the shadcn token mapping (`bg-primary` → `hsl(var(--primary))`). + +### MDX pipeline + +``` +Doc page mounts + └── api.getDoc(filename) → { source, type, metadata } + └── + └── compileMDX(source) // src/lib/mdx.tsx + └── @mdx-js/mdx evaluate() + ├── remark-gfm (tables, task lists, strikethrough) + ├── rehype-highlight (syntax highlighting) + └── useMDXComponents → mdxComponents map + ├── Alert, AlertTitle, AlertDescription + ├── Card, CardHeader, CardTitle, CardDescription, CardContent + ├── Button + ├── Tabs, TabsList, TabsTrigger, TabsContent + └── table/thead/tbody/tr/th/td → shadcn Table primitives +``` + +The whole pipeline runs in the browser. The Go server never sees compiled HTML — it just serves the raw `.md` / `.mdx` source. + +--- + +## Build & distribution + +### Development + +`make dev` runs the Go server (via `air` if installed, else `go run`) and the Vite dev server (`:5173`) concurrently. Vite's dev server has a proxy (`vite.config.ts:14-17`) that forwards `/api/*` and `/query/*` to `http://localhost:8080`, so the React app can call relative URLs and hit the Go backend without CORS gymnastics. + +For breakpoint debugging, see `.zed/debug.json` — three configs: Backend (Go via Delve), Frontend dev server (Vite via Node), Frontend (Chrome). + +### Production + +`make build` runs: + +1. `npm ci && npm run build` inside `web/frontend/` → produces `web/frontend/dist/`. +2. `go build -o build/golinks ./cmd/server` — the `//go:embed all:dist` directive in `web/frontend/embed.go` pulls the dist into the binary. + +Result: a single ~14 MB binary that needs only the `docs/` directory and the SQLite db file at runtime. + +### Docker + +`Dockerfile` is three-stage: + +1. `node:20-alpine` — installs `web/frontend/package*.json`, runs `npm ci`, then `npm run build`. Output: `/app/web/frontend/dist/`. +2. `golang:1.21-alpine` — copies the dist over the top of any committed stub, runs `CGO_ENABLED=1 go build` (CGO needed for the SQLite driver). +3. `alpine:3.18` — final runtime. Copies the binary and the `docs/` directory. **No `web/`** in the runtime image. Drops to a non-root `golinks` user. Exposes 8080. SQLite db lives in `/app/data/`. + +The `HEALTHCHECK` hits `http://localhost:8080/` every 30 s. + +--- + +## Endpoint reference + +Routes are registered in `internal/handlers/handler.go:46-58` (links + redirect) and `internal/handlers/document.go:33-38` (docs), with the SPA catch-all wired in `cmd/server/main.go:75-78`. Match order matters — gorilla/mux matches in registration order. + +### `GET /query/{path:.*}` — golink redirect + +The contract that justifies a server-side rendered handler. Browser search-engine integrations issue plain HTTP requests; they don't run JavaScript, so this MUST be a 302 from the Go server. + +**Flow** (`handler.go:RedirectHandler`): + +1. Strip trailing slash from the captured `path`. +2. Call `linkService.GetLink(ctx, path, "")`. +3. On success → `302` to the resolved URL. +4. On `InvalidQueryError` → `302` to `${BASE_URL}/?missing=` (the SPA picks up the `missing` param and shows a toast). +5. On any other error → `500`. + +**Resolution semantics** (`service/link.go:GetLink`): + +- Look up `word` in `linktable`. If found: + - Log a hit in `queries` (best-effort; logging failure does not fail the request). + - If the stored `link` is itself a keyword (not `http(s)://...`), recurse → enables aliases. + - If the stored `link` contains `{*}`, substitute the `searchTerm` (URL-encoded). + - Return the URL. +- If not found *and* `word` contains spaces, peel the last token off and treat it as a search term (`go google cats` → look up `google cats`, fail, then `google` with searchTerm `cats`). +- If still not found → return `InvalidQueryError`. + +### `GET /api/links` — list keywords + recent queries + +`handler.go:ListLinks`. Returns everything the homepage needs in one call. + +```json +{ + "keywords": [ { "word": "...", "link": "...", "created_at": "..." }, ... ], + "recent_queries":[ { "count": 5, "word": "...", "link": "..." }, ... ], + "base_url": "http://localhost:8080" +} +``` + +- `keywords` ← `linkService.GetAllKeywords()` → all rows from `linktable` filtered to URL-shaped links (skipping aliases). +- `recent_queries` ← `linkService.GetRecentQueries()` → top 20 by count over the last 3 days, joined back to `linktable` for the URL. +- `base_url` ← `cfg.BaseURL`, used by the SPA to display the search-engine URL on the home and setup pages. + +### `POST /api/links` — create or update a link + +`handler.go:CreateLink`. JSON body: `{"word":"...","link":"..."}`. + +1. Decode JSON; trim whitespace on both fields. +2. Call `linkService.UpdateLink`, which validates: + - Non-empty `word` and `link`. + - `word` does not end in `/`. + - `link` starts with `http://` or `https://`. + - `link != word` (no self-loops). +3. Insert a new row into `linktable`. +4. Return `{"success": true}` (200) or `400` with the validation message. + +### `POST /update/` — legacy form-encoded create + +`handler.go:UpdateLinkLegacy`. Same semantics as `POST /api/links`, but accepts `application/x-www-form-urlencoded` (`word=...&link=...`) and returns plain text `Link added successfully!`. Kept so anyone who configured their browser against the pre-migration `/update/` endpoint still works. + +### `GET /api/docs` — list documents + +`document.go:ListDocuments`. Reads `docs/`, returns one entry per `.md` / `.mdx` file. For each file, peeks at the first lines to extract `title` and `description` from YAML frontmatter (`service/document.go:peekFrontmatter`); falls back to the filename without extension. Type is inferred from the extension. + +```json +{ "documents": [ { "title": "...", "description": "...", "type": "markdown|mdx", "path": "sample.md" } ] } +``` + +### `GET /api/docs/{filename}` — fetch raw source + +`document.go:GetDocument`. Returns the full file contents (frontmatter included) plus parsed metadata. + +```json +{ + "source": "---\ntitle: ...\n---\n# ...", + "type": "markdown|mdx", + "metadata": { "title": "...", "description": "...", "type": "...", "path": "...", "metadata": { ...full frontmatter map... } } +} +``` + +If the URL omits the extension (`/api/docs/sample`), the handler tries `.md` first, then `.mdx`. 404 if neither exists. The client compiles MDX in the browser (`web/frontend/src/lib/mdx.tsx`), so the server never renders to HTML. + +### `POST /api/docs` — upload a document + +`document.go:UploadDocument`. `multipart/form-data` with field `file`. + +1. Parse multipart form (10 MB limit). +2. Reject files whose name doesn't end in `.md` or `.mdx`. +3. Sanitize the filename via `filepath.Base` (no path traversal) and write into `docs/`. +4. Return `{success, filename, message, url}` (the `url` is `/docs/{filename}` — a SPA route, not an API route). + +⚠️ **No authentication.** With runtime MDX compilation, an unauthenticated upload is effectively a stored XSS / RCE-in-the-browser vector. Gate this behind auth or restrict uploads to `.md` before public deployment. + +### `DELETE /api/docs/{filename}` — delete a document + +`document.go:DeleteDocument`. Sanitize via `filepath.Base`, `os.Remove`. Returns `{success, message}` or 500. + +### `GET /` and `GET /` — embedded SPA + +The catch-all (`cmd/server/main.go`) hands the request to `frontend.Handler("api", "query")`. Behaviour described in the **`web/frontend/embed.go`** section above. + +--- + +## Configuration + +Loaded by `internal/config/config.go`. All env vars optional; defaults shown. + +| Variable | Default | Used by | +| --------------- | ------------------------ | -------------------------------------------------------- | +| `PORT` | `8080` | `cmd/server/main.go` server bind | +| `DATABASE_PATH` | `golinks.db` | `database.NewSQLiteDB` | +| `BASE_URL` | `http://localhost:8080` | Returned in `/api/links` and used in 302 fallback target | +| `ENVIRONMENT` | `development` | Logged on startup; reserved for future env-aware logic | +| `LOG_LEVEL` | `info` | `logger.Config.Level` | + +`.env` at the repo root is auto-loaded if present (godotenv). See `env.example`. + +--- + +## Security & known TODOs + +- **`POST /api/docs` is unauthenticated.** Combined with runtime MDX compilation, this is the highest-risk gap in the codebase. Mitigations: gate behind a shared token, require auth, or restrict uploads to `.md`. +- **No CSRF protection** on `POST /api/links` or `/update/`. Acceptable for a single-user tool on localhost; revisit if exposed publicly. +- **`getUserID` returns `"DefaultUser"`** unconditionally (`handler.go:getUserID`). Real auth never landed; the `user` column in `linktable` is a placeholder. + +## Future / aspirational + +The `pkg/`, `api/`, `configs/`, and `test/` directories from typical Go layouts are **not** present — this repo doesn't need them yet. Add them only when there's a concrete reason (shared utilities consumed by another binary, gRPC API definitions, etc.). + +OpenTelemetry, distributed rate-limiting, retry/backoff for external calls, circuit breakers — none are wired today and none should be added speculatively. The current scope is a single-instance tool with one external dependency (SQLite). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9541073 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,134 @@ +# CLAUDE.md + +## Project + +**GoLinks** — a minimalist URL shortener inspired by Google's internal golinks system. + +**Stack** +- **Backend:** Go (standard library + `gorilla/mux`), SQLite via `mattn/go-sqlite3`, Clean Architecture under `internal/`. +- **Frontend:** React 18 + TypeScript + Vite, Tailwind CSS with shadcn/ui primitives, TanStack Query for server state, react-hook-form + zod for forms, react-router-dom for client routing, `@mdx-js/mdx` for runtime MDX compilation. +- **Distribution:** Single Go binary. The Vite build output (`web/frontend/dist/`) is embedded via `//go:embed all:dist` in `web/frontend/embed.go` — no `web/` directory exists at runtime. + +See `README.md` for user-facing details and commands. + +## Repository layout + +``` +cmd/server/ Application entrypoint +internal/ +├── config/ Env / dotenv configuration +├── database/ SQLite connection + migrations +├── domain/ Models (json + db tagged) +├── handlers/ HTTP handlers (JSON API + redirect) +├── logger/ Structured logger +├── repository/ Data access layer +└── service/ Business logic +web/frontend/ Vite + React SPA +├── src/components/ App components +├── src/components/ui/ shadcn primitives +├── src/pages/ Route-level pages +├── src/lib/ api.ts, mdx.tsx, utils.ts +├── src/hooks/ Custom hooks +├── public/ Static assets (favicon) +├── dist/ Build output, embedded into the Go binary +└── embed.go go:embed bridge + SPA fallback handler +docs/ User-uploaded .md / .mdx, read from disk at runtime +``` + +## Backend conventions (Go) + +### Architecture +- Clean Architecture layering: handlers → service → repository → database. Handlers never touch the DB directly. +- Interface-driven: handlers depend on service interfaces declared in the handlers package; services depend on repository interfaces. +- Composition over inheritance; small, purpose-built interfaces. +- No global state — wire dependencies through constructors. + +### Code style +- Short, single-purpose functions. Wrap errors with `fmt.Errorf("context: %w", err)`. +- Pass `context.Context` through the call chain; service methods take `ctx` as the first arg. +- Defer closing every resource you open (rows, files, response bodies). +- Validate input at request boundaries — never trust query strings, form values, or JSON bodies. + +### HTTP handlers +- One JSON shape per endpoint, encoded via the `writeJSON(w, status, body)` helper. Don't hand-roll JSON in each handler. +- The golink resolver (`/query/{path:.*}`) MUST stay a server-side 302 — it's the contract that lets browser search-engine integrations work. Never replace it with a client-side redirect. +- `/api/*` is reserved for JSON. The catch-all SPA handler refuses `/api/*` requests defensively as a backstop against route-registration regressions. +- Keep `/update/` (form-encoded) as a legacy alias for old browser configurations until you're sure no one relies on it. + +### Testing +- Table-driven unit tests with parallel execution. +- Mock external interfaces with handwritten mocks colocated in `_test.go` files. The `mockLinkService` in `internal/handlers/handler_test.go` is the reference pattern. +- `go test ./... -race` must pass before merging. + +### Linting & formatting +- `gofmt -s`, `goimports -local golinks`, `golangci-lint run --timeout=3m`. Wired into `make fmt`, `make fix`, `make lint`. +- `make ci` is the full gate: frontend install + build, lint, test, build. + +## Frontend conventions + +### Architecture +- The SPA owns ALL UI. The Go server returns JSON or 302; it never renders HTML except the embedded `index.html`. +- Components stay presentational. **TanStack Query owns server state** — don't reinvent caching with `useEffect` + local state for fetched data. +- Keep components small and colocated by feature. shadcn primitives live under `components/ui/`; app components live one level up. +- Don't introduce a global state library (Redux, Zustand). TanStack Query + URL params + component state cover the surface. + +### Styling +- **Use Tailwind utility classes.** The only bespoke CSS file is `src/index.css`, which holds theme tokens and prose overrides. +- The palette is a Rams-inspired set ported to shadcn HSL CSS variables. `--primary` is Braun orange — keep it as the distinctive accent. Don't hard-code hex values; reference tokens via `bg-primary`, `text-foreground`, `border-border`, etc. +- Long-form rendered documents use `@tailwindcss/typography` `prose` classes with overrides in `index.css`. +- Border radius flows from `--radius` (4px). Don't introduce arbitrary radius values. + +### API client +- All HTTP calls go through `src/lib/api.ts`. Never call `fetch` directly from a component. +- Each endpoint gets a typed wrapper returning a typed response. Add new endpoints there with explicit types. +- Errors throw `ApiError`; query/mutation `onError` handlers display them via `sonner` toast. + +### Forms +- Use `react-hook-form` + `zod`. Define a zod schema, infer the form type, wire `zodResolver`. The `LinkForm` component is the reference. + +### Routing +- `react-router-dom` v6. Routes live in `src/App.tsx`. Page components live under `src/pages/` and never import each other. +- Deep-linkable URLs. State that can be in the URL (filters, search, current tab) should be — use `useSearchParams`. + +### TypeScript +- `tsc -b` must pass. `tsconfig.app.json` has `strict`, `noUnusedLocals`, `noUnusedParameters` enabled. +- No `any` unless interfacing with an unyielding library type. Prefer `unknown` + narrowing. +- Path alias `@/*` maps to `src/*`. + +## MDX + +- Real MDX compilation happens **client-side** via `@mdx-js/mdx`'s `evaluate()` in `src/lib/mdx.tsx`. The server returns raw source from `/api/docs/{filename}`. +- Components exposed to MDX are explicitly enumerated in the `mdxComponents` map. Adding a new component for authors means: import it, add it to that map. No magic auto-discovery. +- `remark-gfm` provides tables, strikethrough, task lists; `rehype-highlight` provides syntax highlighting (GitHub theme). +- **Security:** runtime MDX evaluates JSX as code in the viewer's browser. `POST /api/docs` is unauthenticated — gate it or restrict uploads to `.md` before exposing this app publicly. Tracked as a TODO in `internal/handlers/document.go`. + +## Workflows + +### Development +- `make dev` runs the Go server (with `air` if installed) and the Vite dev server (`:5173`) concurrently. Vite proxies `/api` and `/query` to `:8080`. +- Frontend-only: `make frontend-dev`. Backend-only: `go run ./cmd/server` (the committed stub `dist/index.html` will serve a "build the frontend" page until you run `make frontend-build`). + +### Build +- `make build` runs `npm run build` then `go build`. The Vite output is embedded via `//go:embed all:dist`. +- A stub `dist/index.html` is committed so `git clone && go build` produces a runnable binary even before the frontend has been built. + +### Docker +- Three-stage `Dockerfile`: `node:20-alpine` builds the SPA → `golang:1.21-alpine` builds the binary with the SPA embedded → `alpine:3.18` runtime with only the binary, `docs/`, and the data volume. +- The runtime image must have no `web/` directory. If you see one, the Dockerfile has regressed. + +## Cross-cutting principles + +1. **Single artifact.** One Go binary serves API, redirects, and SPA. Don't introduce a separate frontend service. +2. **Trust the boundary.** Validate at request edges (`internal/handlers/*`, frontend `lib/api.ts`). Inside the boundary, types are honest. +3. **Reuse over rewrite.** Before adding a component or helper, check `components/ui/`, `lib/utils.ts`, `lib/api.ts`, and the service layer. +4. **Boring tech.** Stick to the existing stack unless there's a concrete reason to add a dependency. +5. **Readable, testable, documented.** Every exported Go function gets a GoDoc-style comment. Every JSON endpoint has at least a smoke test. + +## Aspirational (not yet wired) + +These are good practices to apply *if and when* the project grows into them — don't shoehorn them into the current codebase. + +- **OpenTelemetry** tracing, metrics, and structured logs. Adopt once there's an actual observability backend to ship to (Collector, Jaeger, Prometheus, etc.). +- **Distributed rate-limiting** (Redis-backed). Single-instance deployment doesn't need it. +- **Auth on `POST /api/docs`.** Critical before any public deployment because of runtime MDX (see security note above). +- **Retries / circuit breakers / backoff.** Add when external dependencies appear; today there are none beyond SQLite. diff --git a/Dockerfile b/Dockerfile index 6c2f522..c5329f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,61 +1,58 @@ -# Build stage -FROM golang:1.21-alpine AS builder - -# Install build dependencies +# syntax=docker/dockerfile:1.6 + +# ----------------------------------------------------------------------------- +# Stage 1: build the React/Vite frontend into web/frontend/dist +# ----------------------------------------------------------------------------- +FROM node:20-alpine AS frontend +WORKDIR /app/web/frontend +COPY web/frontend/package*.json ./ +RUN npm ci +COPY web/frontend/ ./ +RUN npm run build + +# ----------------------------------------------------------------------------- +# Stage 2: compile the Go binary with the built frontend embedded +# ----------------------------------------------------------------------------- +FROM golang:1.21-alpine AS backend RUN apk add --no-cache gcc musl-dev sqlite-dev - -# Set working directory WORKDIR /app -# Copy go mod files COPY go.mod go.sum ./ - -# Download dependencies RUN go mod download -# Copy source code COPY . . +# Drop any pre-existing dist (in case the repo's stub is present) and copy the +# freshly-built assets over the top so go:embed picks them up. +RUN rm -rf web/frontend/dist +COPY --from=frontend /app/web/frontend/dist ./web/frontend/dist -# Build the application RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o golinks ./cmd/server -# Final stage +# ----------------------------------------------------------------------------- +# Stage 3: minimal runtime with the single binary only — no web/ directory. +# ----------------------------------------------------------------------------- FROM alpine:3.18 - -# Install runtime dependencies RUN apk add --no-cache ca-certificates sqlite tzdata -# Create non-root user RUN addgroup -g 1001 -S golinks && \ adduser -u 1001 -S golinks -G golinks -# Set working directory WORKDIR /app - -# Copy binary from builder stage -COPY --from=builder /app/golinks . - -# Copy web assets -COPY --from=builder /app/web ./web - -# Create data directory for SQLite database +COPY --from=backend /app/golinks . +# docs/ is still read from disk because uploads are persisted there. +COPY --from=backend /app/docs ./docs RUN mkdir -p /app/data && chown -R golinks:golinks /app -# Switch to non-root user USER golinks -# Expose port EXPOSE 8080 -# Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:8080/homepage/ || exit 1 + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1 -# Set environment variables ENV PORT=8080 ENV DATABASE_PATH=/app/data/golinks.db ENV BASE_URL=http://localhost:8080 ENV ENVIRONMENT=production -# Run the application CMD ["./golinks"] diff --git a/Makefile b/Makefile index b3b50f1..2cb4c04 100644 --- a/Makefile +++ b/Makefile @@ -1,74 +1,81 @@ # GoLinks Makefile -# Variables BINARY_NAME=golinks BUILD_DIR=./build +FRONTEND_DIR=./web/frontend -# Go commands GOCMD=go GOBUILD=$(GOCMD) build GOTEST=$(GOCMD) test GOFMT=gofmt -.PHONY: help run build test fmt fix lint clean +.PHONY: help run build dev test fmt fix lint deps clean \ + frontend-install frontend-build frontend-dev \ + docker-build docker-run ci -# Help help: ## Show available commands - @echo 'Available commands:' - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-10s %s\n", $$1, $$2}' + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}' -# Development -run: ## Run the application - @$(GOCMD) run cmd/server/main.go +# --- Frontend --------------------------------------------------------------- -dev: ## Run with hot reload (requires air) - @air || $(GOCMD) run cmd/server/main.go +frontend-install: ## Install frontend dependencies + @cd $(FRONTEND_DIR) && npm ci || (cd $(FRONTEND_DIR) && npm install) -# Building -build: ## Build the binary +frontend-build: ## Build the Vite/React SPA into web/frontend/dist + @cd $(FRONTEND_DIR) && npm run build + +frontend-dev: ## Run the Vite dev server (proxies /api and /query to :8080) + @cd $(FRONTEND_DIR) && npm run dev + +# --- Go --------------------------------------------------------------------- + +run: frontend-build ## Run the application (builds frontend first) + @$(GOCMD) run ./cmd/server + +dev: ## Run Go (air if available) and Vite dev server concurrently + @command -v air >/dev/null 2>&1 && \ + (trap 'kill 0' INT TERM; air & (cd $(FRONTEND_DIR) && npm run dev); wait) || \ + (trap 'kill 0' INT TERM; $(GOCMD) run ./cmd/server & (cd $(FRONTEND_DIR) && npm run dev); wait) + +build: frontend-build ## Build the single-binary production artifact @mkdir -p $(BUILD_DIR) - @$(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME) cmd/server/main.go + @$(GOBUILD) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/server -# Testing -test: ## Run tests +test: ## Run Go tests @$(GOTEST) -v -race ./... -# Code quality -fmt: ## Format code and check formatting - @echo "Formatting code..." +fmt: ## Format Go code and check formatting @$(GOFMT) -s -w . @$(GOCMD) mod tidy - @echo "Checking formatting..." @test -z "$$($(GOFMT) -s -l .)" && echo "✓ Code is properly formatted" || (echo "✗ Code formatting issues found" && exit 1) -fix: ## Fix formatting and auto-fixable linting issues - @echo "Fixing code formatting..." +fix: ## Auto-fix Go formatting and linting @$(GOFMT) -s -w . - @echo "Running goimports..." @which goimports > /dev/null || go install golang.org/x/tools/cmd/goimports@latest @goimports -w -local golinks . - @echo "Fixing auto-fixable linting issues..." @which golangci-lint > /dev/null || (echo "golangci-lint not found. Installing..." && go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest) @golangci-lint run --fix --timeout=3m ./... || echo "Some issues may require manual fixing" - @echo "Code formatting and auto-fixes complete!" -lint: ## Run linter +lint: ## Run Go linter @which golangci-lint > /dev/null || (echo "golangci-lint not found. Installing..." && go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest) @golangci-lint run --timeout=3m ./... -# Dependencies -deps: ## Download dependencies +deps: ## Download Go dependencies @$(GOCMD) mod download @$(GOCMD) mod tidy -# Docker +ci: frontend-install frontend-build lint test build ## Run the full CI pipeline + +# --- Docker ----------------------------------------------------------------- + docker-build: ## Build Docker image @docker build -t $(BINARY_NAME) . docker-run: ## Run Docker container @docker run -p 8080:8080 --rm $(BINARY_NAME) -# Cleanup +# --- Cleanup ---------------------------------------------------------------- + clean: ## Clean build artifacts - @rm -rf $(BUILD_DIR) - @rm -f *.db \ No newline at end of file + @rm -rf $(BUILD_DIR) $(FRONTEND_DIR)/dist + @rm -f *.db diff --git a/README.md b/README.md index 542eb5e..802aa0e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ A modern, minimalist URL shortener inspired by Google's internal golinks system. - **Simple URL Shortening**: Create memorable shortcuts for long URLs - **Variable Substitution**: Use `{*}` placeholders for dynamic content -- **Recursive Aliases**: Keywords can point to other keywords - **Usage Analytics**: Track popular queries and usage patterns - **Clean Architecture**: Modular, testable, and maintainable codebase - **Modern UI**: HTMX-powered interface with Dieter Rams-inspired design @@ -203,10 +202,6 @@ export ENVIRONMENT=production 4. Add tests 5. Submit a pull request -## License - -MIT License - see LICENSE file for details. - ## Acknowledgments - Inspired by Google's internal golinks system diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..cd73c25 --- /dev/null +++ b/TODO.md @@ -0,0 +1,63 @@ +# TODO + +Roadmap for GoLinks. Items are grouped by theme and ordered roughly by dependency — top sections unblock lower ones. Each item has a short scope line so Claude (or future-you) can pick one up without re-deriving context. See `ARCHITECTURE.md` for the underlying design and `CLAUDE.md` for conventions. + +## Suggested order + +1. **MCP server (Phase 1)** — independent, ships immediate value, can run before broader auth via a shared bearer token. +2. **Authentication** — unblocks the MDX upload security gap and is required for everything below. +3. **Authorization & roles** — depends on auth. +4. **Admin features** (edit/delete UI, request/approval flow) — depend on roles. +5. **Tooling** (migrations library) and **independent features** (dark mode, doc analytics) — do whenever. + +--- + +## MCP server — Phase 1 (GoLinks-native) + +The aim: let agents resolve, search, read, and create golinks via MCP. **Phase 1 only exposes GoLinks' own data (`linktable` + `docs/`).** Federated search across third-party sources is intentionally a Phase 2 decision, gated on real usage gaps observed after Phase 1 ships — see *Open questions* below. + +- [ ] **Add `internal/mcp/` server package.** Use `github.com/mark3labs/mcp-go` for protocol plumbing (HTTP-streamable transport, since this is team-shared). Reuse the existing `LinkService` and `DocumentService` — no business logic in the MCP layer. +- [ ] **`MCP_TOKEN` bearer middleware.** Read from env, validate `Authorization: Bearer ` on every MCP request. Reject with 401 on miss. Document in `env.example`. +- [ ] **Mount at `/mcp`** in `cmd/server/main.go` after `/api/*` and before the SPA catch-all. Add `mcp` to the `frontend.Handler` reserved-prefix list. +- [ ] **FTS5 index over `linktable`.** New migration: virtual table mirroring `word + link`, with INSERT/UPDATE/DELETE triggers to keep it in sync. Required for `search_golinks`. +- [ ] **FTS5 index over `docs/`.** Built on startup, updated on upload and delete in `DocumentService`. Strip frontmatter before indexing. Required for `search_docs`. +- [ ] **Tool: `resolve_golink(word: string)`** — wraps `LinkService.GetLink`. Returns `{url}` on hit, error on miss. +- [ ] **Tool: `search_golinks(query: string, limit?: int = 10)`** — FTS5 query over `linktable`. Returns `[{word, link, score}]`. +- [ ] **Tool: `list_golinks(limit?: int = 100, offset?: int = 0)`** — wraps `LinkService.GetAllKeywords` with pagination. Returns `[{word, link, created_at}]`. +- [ ] **Tool: `search_docs(query: string, limit?: int = 10)`** — FTS5 query over the docs index. Returns `[{filename, title, snippet, score}]`. +- [ ] **Tool: `fetch_doc(filename: string)`** — wraps `DocumentService.GetDocument`. Returns `{source, type, metadata}`. +- [ ] **Tool: `create_golink(word: string, url: string)`** — wraps `LinkService.UpdateLink`. Returns `{success}` or validation error. +- [ ] **Smoke tests in `internal/mcp/server_test.go`** — table-driven, with the same mock pattern as `handler_test.go`. Cover: token rejection, each tool happy path, search with no results, validation errors. +- [ ] **Update `ARCHITECTURE.md`** with the `/mcp` endpoint, tool catalog, and the Phase 1/Phase 2 scope decision. Update `CLAUDE.md`'s endpoint list and add a short "MCP conventions" section. +- [ ] **Update `README.md`** with a "Connecting an agent" section: example MCP client config (Claude Desktop / Claude Code) using `MCP_TOKEN`. + +## Authentication & authorization + +- [ ] **Authentication.** Pick an approach and implement it: (a) proprietary email+password with bcrypt, (b) OAuth via GitHub/Google, (c) shared session token. Today `getUserID` in `internal/handlers/handler.go` returns `"DefaultUser"` unconditionally — replace it with a real identity lookup. Blocks the runtime MDX upload risk flagged in `ARCHITECTURE.md` and `CLAUDE.md`. +- [ ] **Authorization with roles.** Two roles to start: `admin` (full CRUD on golinks and docs) and `user` (read, search, propose). Gate `POST /api/links`, `POST /api/docs`, `DELETE /api/docs/*`, and `create_golink` MCP tool on `admin`. _Depends on authentication._ + +## Tooling + +- [ ] **Switch to `goose` (or `golang-migrate`).** Migrations today are an inline string slice in `internal/database/sqlite.go:Migrate`. As the schema grows (auth tables, FTS5, doc analytics), versioned migration files become important for safe rollback and review. + +## Features + +- [ ] **Dark mode.** shadcn theming is already token-driven — add a `.dark` block in `web/frontend/src/index.css` overriding the HSL variables. Toggle via a ` + + + +## Standard markdown still works + +Everything from [sample.md](/docs/sample.md) renders identically here — MDX is +a strict superset of markdown. + +### Code blocks + +```typescript +import { evaluate } from "@mdx-js/mdx"; +import * as runtime from "react/jsx-runtime"; + +const { default: Content } = await evaluate(source, { + ...runtime, + useMDXComponents: () => mdxComponents, +}); +``` + +### Tables + +| Component | Source | Variants | +| --------- | --------------------------------- | ------------------------------------- | +| `Alert` | `components/ui/alert.tsx` | `default`, `info`, `success`, `destructive` | +| `Card` | `components/ui/card.tsx` | — | +| `Tabs` | `components/ui/tabs.tsx` | — | +| `Button` | `components/ui/button.tsx` | `default`, `outline`, `ghost`, `link`, `secondary`, `destructive` | + +### Blockquote + +> The best documentation has a working example one click away. + +--- + +*Edit this file or upload a new one from the [docs page](/docs) to try things out.* diff --git a/env.example b/env.example index bf201f4..ab1d315 100644 --- a/env.example +++ b/env.example @@ -7,4 +7,9 @@ BASE_URL=http://localhost:8080 # Database Configuration DATABASE_PATH=golinks.db +# Environment Configuration ENVIRONMENT=development + +# Logging Configuration +# LOG_LEVEL: debug, info, warn, error (default: info) +LOG_LEVEL=debug diff --git a/go.mod b/go.mod index 2282faf..a17554c 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,5 @@ require ( github.com/gorilla/mux v1.8.1 github.com/joho/godotenv v1.5.1 github.com/mattn/go-sqlite3 v1.14.18 + gopkg.in/yaml.v2 v2.3.0 ) diff --git a/go.sum b/go.sum index 3838145..21f8aee 100644 --- a/go.sum +++ b/go.sum @@ -4,3 +4,7 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/config/config.go b/internal/config/config.go index ae61c68..540bcf0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,15 +4,18 @@ import ( "os" "strconv" + "golinks/internal/logger" + "github.com/joho/godotenv" ) // Config holds all configuration for the application type Config struct { - Port int `json:"port"` - DatabasePath string `json:"database_path"` - BaseURL string `json:"base_url"` - Environment string `json:"environment"` + Port int `json:"port"` + DatabasePath string `json:"database_path"` + BaseURL string `json:"base_url"` + Environment string `json:"environment"` + Logging logger.Config `json:"logging"` } // Load loads configuration from environment variables and .env file @@ -25,6 +28,10 @@ func Load() (*Config, error) { DatabasePath: getEnv("DATABASE_PATH", "golinks.db"), BaseURL: getEnv("BASE_URL", "http://localhost:8080"), Environment: getEnv("ENVIRONMENT", "development"), + Logging: logger.Config{ + Level: getEnv("LOG_LEVEL", "info"), + Format: "text", // Not used in simple logger + }, } return cfg, nil diff --git a/internal/domain/models.go b/internal/domain/models.go index dfd3519..5f88fa2 100644 --- a/internal/domain/models.go +++ b/internal/domain/models.go @@ -40,10 +40,9 @@ type PopularQuery struct { Link string `json:"link"` } -// KeywordInfo represents keyword information with aliases +// KeywordInfo represents keyword information type KeywordInfo struct { Word string `json:"word"` - Aliases string `json:"aliases"` Link string `json:"link"` CreatedAt time.Time `json:"created_at"` } diff --git a/internal/handlers/document.go b/internal/handlers/document.go new file mode 100644 index 0000000..fc1c5b8 --- /dev/null +++ b/internal/handlers/document.go @@ -0,0 +1,132 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "golinks/internal/logger" + "golinks/internal/service" + + "github.com/gorilla/mux" +) + +// DocumentHandler exposes the docs CRUD as JSON. +// All server-side HTML rendering has been removed — the React client does +// MDX compilation in the browser via @mdx-js/mdx. +type DocumentHandler struct { + docService *service.DocumentService + logger *logger.Logger +} + +// NewDocumentHandler creates a new document handler. +func NewDocumentHandler(docService *service.DocumentService, log *logger.Logger) *DocumentHandler { + log.Info("Document handler initialized") + return &DocumentHandler{ + docService: docService, + logger: log, + } +} + +// RegisterRoutes wires the /api/docs endpoints. +func (h *DocumentHandler) RegisterRoutes(router *mux.Router) { + router.HandleFunc("/api/docs", h.ListDocuments).Methods("GET") + router.HandleFunc("/api/docs", h.UploadDocument).Methods("POST") + router.HandleFunc("/api/docs/{filename}", h.GetDocument).Methods("GET") + router.HandleFunc("/api/docs/{filename}", h.DeleteDocument).Methods("DELETE") +} + +// GetDocument returns the raw source and metadata of a single document. +func (h *DocumentHandler) GetDocument(w http.ResponseWriter, r *http.Request) { + filename := mux.Vars(r)["filename"] + if filename == "" { + http.Error(w, "Filename is required", http.StatusBadRequest) + return + } + + // Accept both "sample" and "sample.md"; try .md then .mdx when no extension. + if !strings.HasSuffix(filename, ".md") && !strings.HasSuffix(filename, ".mdx") { + if doc, err := h.docService.GetDocument(r.Context(), filename+".md"); err == nil { + writeJSON(w, http.StatusOK, doc) + return + } + filename = filename + ".mdx" + } + + doc, err := h.docService.GetDocument(r.Context(), filename) + if err != nil { + http.Error(w, fmt.Sprintf("Document not found: %v", err), http.StatusNotFound) + return + } + writeJSON(w, http.StatusOK, doc) +} + +// UploadDocument persists an uploaded .md/.mdx file. +// +// TODO(auth): currently unauthenticated. Runtime MDX evaluates JSX as code on +// the client — gate this behind auth before deploying publicly. +func (h *DocumentHandler) UploadDocument(w http.ResponseWriter, r *http.Request) { + if err := r.ParseMultipartForm(10 << 20); err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + return + } + + file, header, err := r.FormFile("file") + if err != nil { + http.Error(w, "No file provided", http.StatusBadRequest) + return + } + defer file.Close() + + filename := header.Filename + if !strings.HasSuffix(filename, ".md") && !strings.HasSuffix(filename, ".mdx") { + http.Error(w, "Only .md and .mdx files are allowed", http.StatusBadRequest) + return + } + + if err := h.docService.SaveDocument(r.Context(), filename, file); err != nil { + http.Error(w, fmt.Sprintf("Failed to save document: %v", err), http.StatusInternalServerError) + return + } + + writeJSON(w, http.StatusOK, map[string]interface{}{ + "success": true, + "filename": filename, + "message": "Document uploaded successfully", + "url": fmt.Sprintf("/docs/%s", filename), + }) +} + +// ListDocuments returns metadata for every document on disk. +func (h *DocumentHandler) ListDocuments(w http.ResponseWriter, r *http.Request) { + docs, err := h.docService.ListDocuments(r.Context()) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to list documents: %v", err), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, map[string]interface{}{"documents": docs}) +} + +// DeleteDocument removes a document. +func (h *DocumentHandler) DeleteDocument(w http.ResponseWriter, r *http.Request) { + filename := mux.Vars(r)["filename"] + if filename == "" { + http.Error(w, "Filename is required", http.StatusBadRequest) + return + } + if err := h.docService.DeleteDocument(r.Context(), filename); err != nil { + http.Error(w, fmt.Sprintf("Failed to delete document: %v", err), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, map[string]interface{}{ + "success": true, + "message": "Document deleted successfully", + }) +} + +func writeJSON(w http.ResponseWriter, status int, body interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(body) +} diff --git a/internal/handlers/handler.go b/internal/handlers/handler.go index 2134af2..c037371 100644 --- a/internal/handlers/handler.go +++ b/internal/handlers/handler.go @@ -4,19 +4,18 @@ import ( "context" "encoding/json" "fmt" - "html/template" - "log" "net/http" "strings" "golinks/internal/config" "golinks/internal/domain" + "golinks/internal/logger" "golinks/internal/service" "github.com/gorilla/mux" ) -// LinkService interface for link operations +// LinkService is the subset of the link service the HTTP layer depends on. type LinkService interface { GetLink(ctx context.Context, word string, searchTerm string) (string, error) UpdateLink(ctx context.Context, req domain.LinkRequest, userID string) error @@ -24,179 +23,137 @@ type LinkService interface { GetAllKeywords(ctx context.Context) ([]domain.KeywordInfo, error) } -// Handler holds the HTTP handlers +// Handler owns the redirect + JSON endpoints for links. +// The SPA fallback (serving index.html for unmatched routes) lives in the +// main package so it can reference the embedded frontend filesystem. type Handler struct { linkService LinkService config *config.Config - templates *template.Template + logger *logger.Logger } -// NewHandler creates a new handler -func NewHandler(linkService LinkService, cfg *config.Config) *Handler { - // Load templates - templates := template.Must(template.New("").Funcs(template.FuncMap{ - "urlify": func(url string) template.HTML { - if strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "http://") { - return template.HTML(fmt.Sprintf(`%s`, url, url)) - } - return template.HTML(url) - }, - }).ParseGlob("web/templates/*.html")) - +// NewHandler builds a new Handler. No templates are loaded — the UI is a +// React SPA served from the embedded frontend filesystem. +func NewHandler(linkService LinkService, cfg *config.Config, log *logger.Logger) *Handler { + log.Info("Handler initialized (JSON + redirect only)") return &Handler{ linkService: linkService, config: cfg, - templates: templates, + logger: log, } } -// RegisterRoutes registers all HTTP routes +// RegisterRoutes wires the redirect and JSON endpoints. Static-asset and SPA +// fallback handling is registered separately in cmd/server/main.go. func (h *Handler) RegisterRoutes(router *mux.Router) { - // Static files - router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("web/static/")))) - - // API routes + // The golink contract: server-side 302 so browser search-engine integrations work. router.HandleFunc("/query/{path:.*}", h.RedirectHandler).Methods("GET") - router.HandleFunc("/update/", h.UpdateLinkHandler).Methods("POST") - router.HandleFunc("/homepage/", h.HomepageHandler).Methods("GET") - router.HandleFunc("/setup/", h.SetupHandler).Methods("GET") - - // Root redirect to homepage - router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/homepage/", http.StatusFound) - }).Methods("GET") + + // JSON API. The SPA uses only these. + router.HandleFunc("/api/links", h.ListLinks).Methods("GET") + router.HandleFunc("/api/links", h.CreateLink).Methods("POST") + + // Back-compat aliases for the old template-era endpoints so browser + // engines configured against /update/ and /homepage/ keep working. + router.HandleFunc("/update/", h.UpdateLinkLegacy).Methods("POST") } -// RedirectHandler handles golink redirects +// RedirectHandler resolves a golink and issues a 302. func (h *Handler) RedirectHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - - vars := mux.Vars(r) - queryPath := vars["path"] - queryPath = strings.TrimSuffix(queryPath, "/") - + queryPath := strings.TrimSuffix(mux.Vars(r)["path"], "/") userID := h.getUserID(r) + h.logger.Info("Processing golink redirect: %s (user: %s)", queryPath, userID) + targetURL, err := h.linkService.GetLink(ctx, queryPath, "") if err != nil { if _, ok := err.(service.InvalidQueryError); ok { - // Redirect to homepage with missing query parameter - redirectURL := fmt.Sprintf("%s/homepage/?missing=%s", h.config.BaseURL, queryPath) - http.Redirect(w, r, redirectURL, http.StatusFound) + h.logger.Warn("Invalid query '%s' - redirecting to home: %v", queryPath, err) + http.Redirect(w, r, fmt.Sprintf("%s/?missing=%s", h.config.BaseURL, queryPath), http.StatusFound) return } - + h.logger.Error("Failed to get link for query '%s': %v", queryPath, err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } - log.Printf("query word=%s user=%s response=%s", queryPath, userID, targetURL) http.Redirect(w, r, targetURL, http.StatusFound) } -// UpdateLinkHandler handles link creation/updates -func (h *Handler) UpdateLinkHandler(w http.ResponseWriter, r *http.Request) { +// ListLinks returns everything the homepage needs in one call. +func (h *Handler) ListLinks(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + recent, err := h.linkService.GetRecentQueries(ctx) + if err != nil { + h.logger.Error("Failed to get recent queries: %v", err) + recent = []domain.PopularQuery{} + } + + keywords, err := h.linkService.GetAllKeywords(ctx) + if err != nil { + h.logger.Error("Failed to get all keywords: %v", err) + keywords = []domain.KeywordInfo{} + } + + writeJSON(w, http.StatusOK, map[string]interface{}{ + "keywords": keywords, + "recent_queries": recent, + "base_url": h.config.BaseURL, + }) +} + +// CreateLink accepts a JSON {"word":"", "link":""} body. +func (h *Handler) CreateLink(w http.ResponseWriter, r *http.Request) { var req domain.LinkRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid JSON", http.StatusBadRequest) + http.Error(w, "Invalid JSON body", http.StatusBadRequest) return } + req.Word = strings.TrimSpace(req.Word) + req.Link = strings.TrimSpace(req.Link) - userID := h.getUserID(r) - - if err := h.linkService.UpdateLink(ctx, req, userID); err != nil { + if err := h.linkService.UpdateLink(r.Context(), req, h.getUserID(r)); err != nil { if _, ok := err.(service.InvalidQueryError); ok { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]string{"detail": err.Error()}) + h.logger.Warn("Invalid link request word='%s': %v", req.Word, err) + http.Error(w, err.Error(), http.StatusBadRequest) return } - + h.logger.Error("Failed to create link word='%s': %v", req.Word, err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } - log.Printf("update word=%s user=%s link=%s", req.Word, userID, req.Link) - - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(map[string]string{"status": "success"}) + h.logger.Info("Link created: word='%s' link='%s'", req.Word, req.Link) + writeJSON(w, http.StatusOK, map[string]interface{}{"success": true}) } -// HomepageHandler handles the homepage -func (h *Handler) HomepageHandler(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - userID := h.getUserID(r) - - // Get query parameters - success := r.URL.Query().Get("success") - failure := r.URL.Query().Get("failure") - reason := r.URL.Query().Get("reason") - missing := r.URL.Query().Get("missing") - - // Get recent queries and keywords - recentQueries, err := h.linkService.GetRecentQueries(ctx) - if err != nil { - log.Printf("Failed to get recent queries: %v", err) - recentQueries = []domain.PopularQuery{} - } - - allKeywords, err := h.linkService.GetAllKeywords(ctx) - if err != nil { - log.Printf("Failed to get all keywords: %v", err) - allKeywords = []domain.KeywordInfo{} - } - - log.Printf("homepage user=%s", userID) - - data := struct { - Success string - Failure string - Reason string - Missing string - RecentQueries []domain.PopularQuery - AllKeywords []domain.KeywordInfo - BaseURL string - }{ - Success: success, - Failure: failure, - Reason: reason, - Missing: missing, - RecentQueries: recentQueries, - AllKeywords: allKeywords, - BaseURL: h.config.BaseURL, - } - - w.Header().Set("Content-Type", "text/html") - if err := h.templates.ExecuteTemplate(w, "homepage.html", data); err != nil { - log.Printf("Failed to execute template: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) +// UpdateLinkLegacy preserves the old /update/ form endpoint so browser search +// engines that still POST form-encoded data keep working. +func (h *Handler) UpdateLinkLegacy(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "Invalid form data", http.StatusBadRequest) + return } -} - -// SetupHandler handles the setup page -func (h *Handler) SetupHandler(w http.ResponseWriter, r *http.Request) { - userID := h.getUserID(r) - - log.Printf("setup user=%s", userID) - - data := struct { - BaseURL string - }{ - BaseURL: h.config.BaseURL, + req := domain.LinkRequest{ + Word: strings.TrimSpace(r.FormValue("word")), + Link: strings.TrimSpace(r.FormValue("link")), } - - w.Header().Set("Content-Type", "text/html") - if err := h.templates.ExecuteTemplate(w, "setup.html", data); err != nil { - log.Printf("Failed to execute template: %v", err) + if err := h.linkService.UpdateLink(r.Context(), req, h.getUserID(r)); err != nil { + if _, ok := err.(service.InvalidQueryError); ok { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } http.Error(w, "Internal server error", http.StatusInternalServerError) + return } + w.Header().Set("Content-Type", "text/plain") + _, _ = w.Write([]byte("Link added successfully!")) } -// getUserID extracts user ID from request (simplified - no OAuth2 for now) -func (h *Handler) getUserID(r *http.Request) string { - // For now, return a default user. In production, this would extract from OAuth2 cookie +// getUserID extracts the user ID from the request. Authentication is not +// implemented yet — see plan for the follow-up. +func (h *Handler) getUserID(_ *http.Request) string { return "DefaultUser" } diff --git a/internal/handlers/handler_test.go b/internal/handlers/handler_test.go index 4bf16d4..8609517 100644 --- a/internal/handlers/handler_test.go +++ b/internal/handlers/handler_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "html/template" "net/http" "net/http/httptest" "strings" @@ -12,12 +11,12 @@ import ( "golinks/internal/config" "golinks/internal/domain" + "golinks/internal/logger" "golinks/internal/service" "github.com/gorilla/mux" ) -// Mock LinkService for testing type mockLinkService struct { links map[string]string recentQueries []domain.PopularQuery @@ -26,7 +25,7 @@ type mockLinkService struct { getError error } -func (m *mockLinkService) GetLink(ctx context.Context, word string, searchTerm string) (string, error) { +func (m *mockLinkService) GetLink(_ context.Context, word string, _ string) (string, error) { if m.getError != nil { return "", m.getError } @@ -36,7 +35,7 @@ func (m *mockLinkService) GetLink(ctx context.Context, word string, searchTerm s return "", service.InvalidQueryError{Message: "not found"} } -func (m *mockLinkService) UpdateLink(ctx context.Context, req domain.LinkRequest, userID string) error { +func (m *mockLinkService) UpdateLink(_ context.Context, req domain.LinkRequest, _ string) error { if m.updateError != nil { return m.updateError } @@ -44,73 +43,34 @@ func (m *mockLinkService) UpdateLink(ctx context.Context, req domain.LinkRequest return nil } -func (m *mockLinkService) GetRecentQueries(ctx context.Context) ([]domain.PopularQuery, error) { +func (m *mockLinkService) GetRecentQueries(_ context.Context) ([]domain.PopularQuery, error) { return m.recentQueries, nil } -func (m *mockLinkService) GetAllKeywords(ctx context.Context) ([]domain.KeywordInfo, error) { +func (m *mockLinkService) GetAllKeywords(_ context.Context) ([]domain.KeywordInfo, error) { return m.allKeywords, nil } func setupTestHandler() *Handler { - cfg := &config.Config{ - BaseURL: "http://localhost:8080", - } - - // Create simple templates for testing - templates := template.Must(template.New("").Funcs(template.FuncMap{ - "urlify": func(url string) template.HTML { - if strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "http://") { - return template.HTML(`` + url + ``) - } - return template.HTML(url) - }, - }).Parse(` - {{define "homepage.html"}} - - -

GoLinks

- {{if .Missing}}
Missing: {{.Missing}}
{{end}} - {{if .Success}}
Success: {{.Success}}
{{end}} - {{if .Failure}}
Failure: {{.Failure}} - {{.Reason}}
{{end}} -
Recent Queries: {{len .RecentQueries}}
-
All Keywords: {{len .AllKeywords}}
- - - {{end}} - {{define "setup.html"}} - - -

Setup

-

Base URL: {{.BaseURL}}

- - - {{end}} - `)) - - mockService := &mockLinkService{ - links: map[string]string{ - "docs": "https://docs.example.com", - "github": "https://github.com", - }, - recentQueries: []domain.PopularQuery{ - {Count: 5, Word: "docs", Link: "https://docs.example.com"}, - }, - allKeywords: []domain.KeywordInfo{ - {Word: "docs", Link: "https://docs.example.com"}, + return &Handler{ + linkService: &mockLinkService{ + links: map[string]string{ + "docs": "https://docs.example.com", + "github": "https://github.com", + }, + recentQueries: []domain.PopularQuery{ + {Count: 5, Word: "docs", Link: "https://docs.example.com"}, + }, + allKeywords: []domain.KeywordInfo{ + {Word: "docs", Link: "https://docs.example.com"}, + }, }, + config: &config.Config{BaseURL: "http://localhost:8080"}, + logger: logger.Default(), } - - handler := &Handler{ - linkService: mockService, - config: cfg, - templates: templates, - } - - return handler } -func TestHandler_RedirectHandler(t *testing.T) { +func TestRedirectHandler(t *testing.T) { handler := setupTestHandler() tests := []struct { @@ -118,32 +78,11 @@ func TestHandler_RedirectHandler(t *testing.T) { path string expectedStatus int expectedHeader string - setupError error }{ - { - name: "successful redirect", - path: "/query/docs", - expectedStatus: http.StatusFound, - expectedHeader: "https://docs.example.com", - }, - { - name: "missing query redirect to homepage", - path: "/query/nonexistent", - expectedStatus: http.StatusFound, - expectedHeader: "http://localhost:8080/homepage/?missing=nonexistent", - }, - { - name: "empty path", - path: "/query/", - expectedStatus: http.StatusFound, - expectedHeader: "http://localhost:8080/homepage/?missing=", - }, - { - name: "path with trailing slash", - path: "/query/docs/", - expectedStatus: http.StatusFound, - expectedHeader: "https://docs.example.com", - }, + {"hit", "/query/docs", http.StatusFound, "https://docs.example.com"}, + {"miss", "/query/nonexistent", http.StatusFound, "http://localhost:8080/?missing=nonexistent"}, + {"empty", "/query/", http.StatusFound, "http://localhost:8080/?missing="}, + {"trailing slash", "/query/docs/", http.StatusFound, "https://docs.example.com"}, } for _, tt := range tests { @@ -151,246 +90,153 @@ func TestHandler_RedirectHandler(t *testing.T) { req := httptest.NewRequest("GET", tt.path, nil) w := httptest.NewRecorder() - // Setup router to extract path variable router := mux.NewRouter() router.HandleFunc("/query/{path:.*}", handler.RedirectHandler).Methods("GET") router.ServeHTTP(w, req) if w.Code != tt.expectedStatus { - t.Errorf("RedirectHandler() status = %v, want %v", w.Code, tt.expectedStatus) + t.Errorf("status = %v, want %v", w.Code, tt.expectedStatus) } - - if tt.expectedStatus == http.StatusFound { - location := w.Header().Get("Location") - if location != tt.expectedHeader { - t.Errorf("RedirectHandler() Location = %v, want %v", location, tt.expectedHeader) - } + if w.Header().Get("Location") != tt.expectedHeader { + t.Errorf("Location = %q, want %q", w.Header().Get("Location"), tt.expectedHeader) } }) } } -func TestHandler_UpdateLinkHandler(t *testing.T) { +func TestCreateLinkJSON(t *testing.T) { tests := []struct { name string - requestBody interface{} + body string expectedStatus int setupError error }{ { - name: "successful update", - requestBody: domain.LinkRequest{ - Word: "test", - Link: "https://test.com", - }, + name: "valid JSON body", + body: `{"word":"test","link":"https://test.com"}`, expectedStatus: http.StatusOK, }, { - name: "invalid JSON", - requestBody: "invalid json", + name: "malformed JSON", + body: "not json", expectedStatus: http.StatusBadRequest, }, { - name: "service error", - requestBody: domain.LinkRequest{ - Word: "error", - Link: "https://error.com", - }, + name: "service rejects input", + body: `{"word":"bad","link":"https://x.com"}`, expectedStatus: http.StatusBadRequest, - setupError: service.InvalidQueryError{Message: "test error"}, + setupError: service.InvalidQueryError{Message: "bad input"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { handler := setupTestHandler() - - // Setup error if needed if tt.setupError != nil { - mockService := handler.linkService.(*mockLinkService) - mockService.updateError = tt.setupError + handler.linkService.(*mockLinkService).updateError = tt.setupError } - var body []byte - var err error - - if str, ok := tt.requestBody.(string); ok { - body = []byte(str) - } else { - body, err = json.Marshal(tt.requestBody) - if err != nil { - t.Fatalf("Failed to marshal request body: %v", err) - } - } - - req := httptest.NewRequest("POST", "/update/", bytes.NewBuffer(body)) + req := httptest.NewRequest("POST", "/api/links", bytes.NewBufferString(tt.body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() - handler.UpdateLinkHandler(w, req) + handler.CreateLink(w, req) if w.Code != tt.expectedStatus { - t.Errorf("UpdateLinkHandler() status = %v, want %v", w.Code, tt.expectedStatus) - } - - if tt.expectedStatus == http.StatusOK { - var response map[string]string - err := json.NewDecoder(w.Body).Decode(&response) - if err != nil { - t.Errorf("Failed to decode response: %v", err) - } - if response["status"] != "success" { - t.Errorf("Expected success response, got %v", response) - } + t.Errorf("status = %v, want %v, body=%q", w.Code, tt.expectedStatus, w.Body.String()) } }) } } -func TestHandler_HomepageHandler(t *testing.T) { +func TestListLinks(t *testing.T) { handler := setupTestHandler() - tests := []struct { - name string - queryParams string - expectedStatus int - expectedBody []string - }{ - { - name: "basic homepage", - queryParams: "", - expectedStatus: http.StatusOK, - expectedBody: []string{"

GoLinks

", "Recent Queries: 1", "All Keywords: 1"}, - }, - { - name: "homepage with success message", - queryParams: "?success=docs", - expectedStatus: http.StatusOK, - expectedBody: []string{"Success: docs"}, - }, - { - name: "homepage with failure message", - queryParams: "?failure=test&reason=invalid", - expectedStatus: http.StatusOK, - expectedBody: []string{"Failure: test - invalid"}, - }, - { - name: "homepage with missing query", - queryParams: "?missing=nonexistent", - expectedStatus: http.StatusOK, - expectedBody: []string{"Missing: nonexistent"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req := httptest.NewRequest("GET", "/homepage/"+tt.queryParams, nil) - w := httptest.NewRecorder() - - handler.HomepageHandler(w, req) + req := httptest.NewRequest("GET", "/api/links", nil) + w := httptest.NewRecorder() + handler.ListLinks(w, req) - if w.Code != tt.expectedStatus { - t.Errorf("HomepageHandler() status = %v, want %v", w.Code, tt.expectedStatus) - } + if w.Code != http.StatusOK { + t.Fatalf("status = %v, want 200", w.Code) + } + if ct := w.Header().Get("Content-Type"); ct != "application/json" { + t.Errorf("content-type = %q, want application/json", ct) + } - body := w.Body.String() - for _, expected := range tt.expectedBody { - if !strings.Contains(body, expected) { - t.Errorf("HomepageHandler() body should contain %q, got %q", expected, body) - } - } - }) + var resp struct { + Keywords []domain.KeywordInfo `json:"keywords"` + RecentQueries []domain.PopularQuery `json:"recent_queries"` + BaseURL string `json:"base_url"` + } + if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + if len(resp.Keywords) != 1 || resp.Keywords[0].Word != "docs" { + t.Errorf("unexpected keywords: %#v", resp.Keywords) + } + if len(resp.RecentQueries) != 1 { + t.Errorf("unexpected recent queries: %#v", resp.RecentQueries) + } + if resp.BaseURL != "http://localhost:8080" { + t.Errorf("base_url = %q", resp.BaseURL) } } -func TestHandler_SetupHandler(t *testing.T) { +func TestUpdateLinkLegacyForm(t *testing.T) { handler := setupTestHandler() - req := httptest.NewRequest("GET", "/setup/", nil) + form := strings.NewReader("word=leg&link=https://legacy.example.com") + req := httptest.NewRequest("POST", "/update/", form) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() - handler.SetupHandler(w, req) + handler.UpdateLinkLegacy(w, req) if w.Code != http.StatusOK { - t.Errorf("SetupHandler() status = %v, want %v", w.Code, http.StatusOK) + t.Fatalf("status = %v, want 200, body=%q", w.Code, w.Body.String()) } - - body := w.Body.String() - expectedContent := []string{ - "

Setup

", - "Base URL: http://localhost:8080", - } - - for _, expected := range expectedContent { - if !strings.Contains(body, expected) { - t.Errorf("SetupHandler() body should contain %q, got %q", expected, body) - } + if !strings.Contains(w.Body.String(), "Link added successfully") { + t.Errorf("unexpected body: %q", w.Body.String()) } } -func TestHandler_RegisterRoutes(t *testing.T) { +func TestRegisterRoutes(t *testing.T) { handler := setupTestHandler() router := mux.NewRouter() - - // This should not panic handler.RegisterRoutes(router) - // Test that routes are registered by making requests tests := []struct { method string path string + body string status int }{ - {"GET", "/", http.StatusFound}, // Root redirect - {"GET", "/homepage/", http.StatusOK}, // Homepage - {"GET", "/setup/", http.StatusOK}, // Setup - {"GET", "/query/docs", http.StatusFound}, // Query redirect - {"POST", "/update/", http.StatusBadRequest}, // Update (bad request due to no body) + {"GET", "/api/links", "", http.StatusOK}, + {"POST", "/api/links", `{"word":"x","link":"https://x.com"}`, http.StatusOK}, + {"GET", "/query/docs", "", http.StatusFound}, + {"POST", "/update/", "word=x&link=https://x.com", http.StatusOK}, } for _, tt := range tests { t.Run(tt.method+" "+tt.path, func(t *testing.T) { - var req *http.Request - if tt.method == "POST" { - req = httptest.NewRequest(tt.method, tt.path, strings.NewReader("")) - } else { - req = httptest.NewRequest(tt.method, tt.path, nil) + req := httptest.NewRequest(tt.method, tt.path, strings.NewReader(tt.body)) + if tt.method == "POST" && tt.path == "/api/links" { + req.Header.Set("Content-Type", "application/json") + } else if tt.method == "POST" { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } w := httptest.NewRecorder() - router.ServeHTTP(w, req) - if w.Code != tt.status { - t.Errorf("Route %s %s status = %v, want %v", tt.method, tt.path, w.Code, tt.status) + t.Errorf("status = %v, want %v, body=%q", w.Code, tt.status, w.Body.String()) } }) } } -func TestHandler_getUserID(t *testing.T) { - handler := setupTestHandler() - - req := httptest.NewRequest("GET", "/", nil) - userID := handler.getUserID(req) - - // Should return default user since we don't have OAuth2 implemented - if userID != "DefaultUser" { - t.Errorf("getUserID() = %v, want DefaultUser", userID) - } -} - -func TestHandler_MethodNotAllowed(t *testing.T) { +func TestGetUserID(t *testing.T) { handler := setupTestHandler() - router := mux.NewRouter() - handler.RegisterRoutes(router) - - // Test wrong method on homepage - req := httptest.NewRequest("POST", "/homepage/", nil) - w := httptest.NewRecorder() - - router.ServeHTTP(w, req) - - if w.Code != http.StatusMethodNotAllowed { - t.Errorf("Wrong method should return %v, got %v", http.StatusMethodNotAllowed, w.Code) + if uid := handler.getUserID(httptest.NewRequest("GET", "/", nil)); uid != "DefaultUser" { + t.Errorf("getUserID() = %v, want DefaultUser", uid) } } diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..271ddef --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,93 @@ +package logger + +import ( + "fmt" + "log/slog" + "os" +) + +// Logger is a simple wrapper around slog +type Logger struct { + *slog.Logger +} + +// Config holds logger configuration +type Config struct { + Level string `json:"level"` // debug, info, warn, error + Format string `json:"format"` // not used in simple logger +} + +// New creates a new simple slog logger +func New(cfg Config) *Logger { + var level slog.Level + switch cfg.Level { + case "debug": + level = slog.LevelDebug + case "info": + level = slog.LevelInfo + case "warn": + level = slog.LevelWarn + case "error": + level = slog.LevelError + default: + level = slog.LevelInfo + } + + opts := &slog.HandlerOptions{ + Level: level, + AddSource: true, + } + + handler := slog.NewTextHandler(os.Stdout, opts) + logger := slog.New(handler) + + return &Logger{ + Logger: logger, + } +} + +// Debug logs debug messages +func (l *Logger) Debug(msg string, args ...interface{}) { + l.Logger.Debug(formatMessage(msg, args...)) +} + +// Info logs info messages +func (l *Logger) Info(msg string, args ...interface{}) { + l.Logger.Info(formatMessage(msg, args...)) +} + +// Warn logs warning messages +func (l *Logger) Warn(msg string, args ...interface{}) { + l.Logger.Warn(formatMessage(msg, args...)) +} + +// Error logs error messages +func (l *Logger) Error(msg string, args ...interface{}) { + l.Logger.Error(formatMessage(msg, args...)) +} + +// formatMessage formats the message with args using Printf-style formatting +func formatMessage(msg string, args ...interface{}) string { + if len(args) == 0 { + return msg + } + // Use Go's fmt package for printf-style formatting + return fmt.Sprintf(msg, args...) +} + +// Global logger instance +var defaultLogger *Logger + +// Initialize sets up the global logger +func Initialize(cfg Config) { + defaultLogger = New(cfg) +} + +// Default returns the default logger instance +func Default() *Logger { + if defaultLogger == nil { + // Fallback to a basic logger if not initialized + defaultLogger = New(Config{Level: "info", Format: "text"}) + } + return defaultLogger +} diff --git a/internal/repository/query.go b/internal/repository/query.go index 2250d24..ffbb50c 100644 --- a/internal/repository/query.go +++ b/internal/repository/query.go @@ -4,29 +4,43 @@ import ( "context" "database/sql" "fmt" + "time" "golinks/internal/domain" + "golinks/internal/logger" ) // QueryRepository handles database operations for queries type QueryRepository struct { - db *sql.DB + db *sql.DB + logger *logger.Logger } // NewQueryRepository creates a new query repository -func NewQueryRepository(db *sql.DB) *QueryRepository { - return &QueryRepository{db: db} +func NewQueryRepository(db *sql.DB, log *logger.Logger) *QueryRepository { + log.Info("Query repository initialized") + return &QueryRepository{ + db: db, + logger: log, + } } // Create creates a new query log entry func (r *QueryRepository) Create(ctx context.Context, wordID int) error { + start := time.Now() + r.logger.Debug("Creating query log for word ID: %d", wordID) + query := `INSERT INTO queries (word_id, created_at) VALUES (?, CURRENT_TIMESTAMP)` _, err := r.db.ExecContext(ctx, query, wordID) + duration := time.Since(start) + if err != nil { + r.logger.Error("Database insert failed: %v (%v)", err, duration) return fmt.Errorf("failed to create query log: %w", err) } + r.logger.Debug("Query log created successfully (%v)", duration) return nil } @@ -34,6 +48,8 @@ func (r *QueryRepository) Create(ctx context.Context, wordID int) error { func (r *QueryRepository) GetRecentQueries( ctx context.Context, timeWindowDays, numResults int, ) ([]domain.PopularQuery, error) { + start := time.Now() + r.logger.Debug("Getting recent queries: %d days, max %d results", timeWindowDays, numResults) query := ` SELECT COUNT(q.word_id) as count, s.word, s.link @@ -47,6 +63,8 @@ func (r *QueryRepository) GetRecentQueries( rows, err := r.db.QueryContext(ctx, query, timeWindowDays, numResults) if err != nil { + duration := time.Since(start) + r.logger.Error("Database query failed: %v (%v)", err, duration) return nil, fmt.Errorf("failed to get recent queries: %w", err) } defer rows.Close() @@ -56,14 +74,20 @@ func (r *QueryRepository) GetRecentQueries( var pq domain.PopularQuery err := rows.Scan(&pq.Count, &pq.Word, &pq.Link) if err != nil { + duration := time.Since(start) + r.logger.Error("Failed to scan popular query row: %v (%v)", err, duration) return nil, fmt.Errorf("failed to scan popular query: %w", err) } queries = append(queries, pq) } if err := rows.Err(); err != nil { + duration := time.Since(start) + r.logger.Error("Error iterating recent query rows: %v (%v)", err, duration) return nil, fmt.Errorf("error iterating recent queries: %w", err) } + duration := time.Since(start) + r.logger.Debug("Recent queries retrieved successfully: %d queries (%v)", len(queries), duration) return queries, nil } diff --git a/internal/repository/query_test.go b/internal/repository/query_test.go index b9697e8..13739a9 100644 --- a/internal/repository/query_test.go +++ b/internal/repository/query_test.go @@ -5,6 +5,7 @@ import ( "testing" "golinks/internal/domain" + "golinks/internal/logger" ) func TestQueryRepository_Create(t *testing.T) { @@ -12,7 +13,8 @@ func TestQueryRepository_Create(t *testing.T) { defer db.Close() // First create a shortcut to reference - shortcutRepo := NewShortcutRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + shortcutRepo := NewShortcutRepository(db, mockLogger) shortcut := &domain.Shortcut{ Word: "test", Link: "https://test.com", @@ -23,7 +25,7 @@ func TestQueryRepository_Create(t *testing.T) { t.Fatalf("Failed to create test shortcut: %v", err) } - queryRepo := NewQueryRepository(db) + queryRepo := NewQueryRepository(db, mockLogger) tests := []struct { name string @@ -58,8 +60,9 @@ func TestQueryRepository_GetRecentQueries(t *testing.T) { defer db.Close() // Setup test data - shortcutRepo := NewShortcutRepository(db) - queryRepo := NewQueryRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + shortcutRepo := NewShortcutRepository(db, mockLogger) + queryRepo := NewQueryRepository(db, mockLogger) // Create shortcuts shortcuts := []*domain.Shortcut{ @@ -182,8 +185,9 @@ func TestQueryRepository_GetRecentQueries_TimeWindow(t *testing.T) { db := setupTestDB(t) defer db.Close() - shortcutRepo := NewShortcutRepository(db) - queryRepo := NewQueryRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + shortcutRepo := NewShortcutRepository(db, mockLogger) + queryRepo := NewQueryRepository(db, mockLogger) // Create a shortcut shortcut := &domain.Shortcut{ @@ -242,7 +246,8 @@ func TestQueryRepository_DatabaseError(t *testing.T) { db := setupTestDB(t) db.Close() // Close immediately to cause errors - repo := NewQueryRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + repo := NewQueryRepository(db, mockLogger) // Test Create with closed DB err := repo.Create(context.Background(), 1) @@ -261,7 +266,8 @@ func TestQueryRepository_EmptyResults(t *testing.T) { db := setupTestDB(t) defer db.Close() - repo := NewQueryRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + repo := NewQueryRepository(db, mockLogger) // Test GetRecentQueries with no data queries, err := repo.GetRecentQueries(context.Background(), 1, 10) diff --git a/internal/repository/shortcut.go b/internal/repository/shortcut.go index e61a076..4e7b789 100644 --- a/internal/repository/shortcut.go +++ b/internal/repository/shortcut.go @@ -4,22 +4,31 @@ import ( "context" "database/sql" "fmt" + "time" "golinks/internal/domain" + "golinks/internal/logger" ) // ShortcutRepository handles database operations for shortcuts type ShortcutRepository struct { - db *sql.DB + db *sql.DB + logger *logger.Logger } // NewShortcutRepository creates a new shortcut repository -func NewShortcutRepository(db *sql.DB) *ShortcutRepository { - return &ShortcutRepository{db: db} +func NewShortcutRepository(db *sql.DB, log *logger.Logger) *ShortcutRepository { + log.Info("Shortcut repository initialized") + return &ShortcutRepository{ + db: db, + logger: log, + } } // GetByWord retrieves the most recent shortcut by word func (r *ShortcutRepository) GetByWord(ctx context.Context, word string) (*domain.Shortcut, error) { + start := time.Now() + r.logger.Debug("Getting shortcut by word: %s", word) query := ` SELECT id, word, link, user, created_at @@ -38,18 +47,25 @@ func (r *ShortcutRepository) GetByWord(ctx context.Context, word string) (*domai &shortcut.CreatedAt, ) + duration := time.Since(start) + if err == sql.ErrNoRows { + r.logger.Debug("No shortcut found for word '%s' (%v)", word, duration) return nil, nil } if err != nil { + r.logger.Error("Database query failed for word '%s': %v (%v)", word, err, duration) return nil, fmt.Errorf("failed to get shortcut by word: %w", err) } + r.logger.Debug("Shortcut retrieved: id=%d user='%s' (%v)", shortcut.ID, shortcut.User, duration) return &shortcut, nil } // Create creates a new shortcut func (r *ShortcutRepository) Create(ctx context.Context, shortcut *domain.Shortcut) error { + start := time.Now() + r.logger.Debug("Creating shortcut: word='%s' link='%s' user='%s'", shortcut.Word, shortcut.Link, shortcut.User) query := ` INSERT INTO linktable (word, link, user, created_at) @@ -57,21 +73,28 @@ func (r *ShortcutRepository) Create(ctx context.Context, shortcut *domain.Shortc ` result, err := r.db.ExecContext(ctx, query, shortcut.Word, shortcut.Link, shortcut.User) + duration := time.Since(start) + if err != nil { + r.logger.Error("Database insert failed: %v (%v)", err, duration) return fmt.Errorf("failed to create shortcut: %w", err) } id, err := result.LastInsertId() if err != nil { + r.logger.Error("Failed to get last insert ID: %v (%v)", err, duration) return fmt.Errorf("failed to get last insert id: %w", err) } shortcut.ID = int(id) + r.logger.Info("Shortcut created successfully: id=%d (%v)", shortcut.ID, duration) return nil } // GetAllKeywords retrieves all keywords with their latest links func (r *ShortcutRepository) GetAllKeywords(ctx context.Context) ([]domain.KeywordInfo, error) { + start := time.Now() + r.logger.Debug("Getting all keywords") query := ` SELECT word, link, created_at, MAX(id) as max_id @@ -82,6 +105,8 @@ func (r *ShortcutRepository) GetAllKeywords(ctx context.Context) ([]domain.Keywo rows, err := r.db.QueryContext(ctx, query) if err != nil { + duration := time.Since(start) + r.logger.Error("Database query failed: %v (%v)", err, duration) return nil, fmt.Errorf("failed to get all keywords: %w", err) } defer rows.Close() @@ -92,14 +117,20 @@ func (r *ShortcutRepository) GetAllKeywords(ctx context.Context) ([]domain.Keywo var maxID int err := rows.Scan(&keyword.Word, &keyword.Link, &keyword.CreatedAt, &maxID) if err != nil { + duration := time.Since(start) + r.logger.Error("Failed to scan keyword row: %v (%v)", err, duration) return nil, fmt.Errorf("failed to scan keyword: %w", err) } keywords = append(keywords, keyword) } if err := rows.Err(); err != nil { + duration := time.Since(start) + r.logger.Error("Error iterating keyword rows: %v (%v)", err, duration) return nil, fmt.Errorf("error iterating keywords: %w", err) } + duration := time.Since(start) + r.logger.Debug("All keywords retrieved successfully: %d keywords (%v)", len(keywords), duration) return keywords, nil } diff --git a/internal/repository/shortcut_test.go b/internal/repository/shortcut_test.go index 343637c..01c1f6b 100644 --- a/internal/repository/shortcut_test.go +++ b/internal/repository/shortcut_test.go @@ -7,6 +7,7 @@ import ( "time" "golinks/internal/domain" + "golinks/internal/logger" _ "github.com/mattn/go-sqlite3" ) @@ -49,7 +50,8 @@ func TestShortcutRepository_GetByWord(t *testing.T) { db := setupTestDB(t) defer db.Close() - repo := NewShortcutRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + repo := NewShortcutRepository(db, mockLogger) // Insert test data testShortcut := &domain.Shortcut{ @@ -126,7 +128,8 @@ func TestShortcutRepository_Create(t *testing.T) { db := setupTestDB(t) defer db.Close() - repo := NewShortcutRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + repo := NewShortcutRepository(db, mockLogger) tests := []struct { name string @@ -194,7 +197,8 @@ func TestShortcutRepository_GetAllKeywords(t *testing.T) { db := setupTestDB(t) defer db.Close() - repo := NewShortcutRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + repo := NewShortcutRepository(db, mockLogger) // Insert test data testShortcuts := []*domain.Shortcut{ @@ -244,7 +248,8 @@ func TestShortcutRepository_GetByWord_MostRecent(t *testing.T) { db := setupTestDB(t) defer db.Close() - repo := NewShortcutRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + repo := NewShortcutRepository(db, mockLogger) // Create multiple versions of the same word shortcuts := []*domain.Shortcut{ @@ -289,7 +294,8 @@ func TestShortcutRepository_DatabaseError(t *testing.T) { db := setupTestDB(t) db.Close() // Close immediately to cause errors - repo := NewShortcutRepository(db) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + repo := NewShortcutRepository(db, mockLogger) // Test GetByWord with closed DB _, err := repo.GetByWord(context.Background(), "test") diff --git a/internal/service/document.go b/internal/service/document.go new file mode 100644 index 0000000..3244da4 --- /dev/null +++ b/internal/service/document.go @@ -0,0 +1,222 @@ +package service + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "golinks/internal/logger" + + "gopkg.in/yaml.v2" +) + +// DocumentService stores and retrieves markdown/MDX documents on disk. +// +// Compared to the previous incarnation this service does NOT render the +// documents server-side: the Vite/React frontend handles MDX compilation at +// runtime via @mdx-js/mdx. The Go side is limited to file I/O plus lightweight +// frontmatter parsing so the client knows the title/description without +// having to parse the whole document twice. +type DocumentService struct { + docsPath string + logger *logger.Logger +} + +// DocumentInfo contains metadata about a document. +type DocumentInfo struct { + Title string `json:"title"` + Description string `json:"description,omitempty"` + Type string `json:"type"` // "markdown" or "mdx" + Path string `json:"path"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// DocumentSource is the raw file contents plus its parsed metadata. +// The client receives this untouched and compiles MDX in the browser. +type DocumentSource struct { + Source string `json:"source"` + Type string `json:"type"` + Metadata DocumentInfo `json:"metadata"` +} + +// NewDocumentService creates a new document service rooted at docsPath. +func NewDocumentService(docsPath string, log *logger.Logger) *DocumentService { + log.Info("Initializing document service: %s", docsPath) + return &DocumentService{ + docsPath: docsPath, + logger: log, + } +} + +// GetDocument reads a document by filename and returns its raw source plus metadata. +func (s *DocumentService) GetDocument(ctx context.Context, filename string) (*DocumentSource, error) { + _ = ctx + filename = filepath.Base(filename) + filePath := filepath.Join(s.docsPath, filename) + + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return nil, fmt.Errorf("document not found: %s", filename) + } + + content, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read document: %w", err) + } + + docType := "markdown" + if strings.HasSuffix(filename, ".mdx") { + docType = "mdx" + } + + metaData, body := splitFrontmatter(content) + info := DocumentInfo{ + Title: getStringFromMeta(metaData, "title", strings.TrimSuffix(filename, filepath.Ext(filename))), + Description: getStringFromMeta(metaData, "description", ""), + Type: docType, + Path: filename, + Metadata: metaData, + } + + // Hand back the full source (including frontmatter) so the client can + // decide whether to strip it itself. remark/MDX pipelines tolerate both. + _ = body + return &DocumentSource{ + Source: string(content), + Type: docType, + Metadata: info, + }, nil +} + +// SaveDocument writes a document to disk, creating or overwriting. +func (s *DocumentService) SaveDocument(ctx context.Context, filename string, content io.Reader) error { + _ = ctx + filename = filepath.Base(filename) + filePath := filepath.Join(s.docsPath, filename) + + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create document file: %w", err) + } + defer file.Close() + + if _, err := io.Copy(file, content); err != nil { + return fmt.Errorf("failed to write document content: %w", err) + } + return nil +} + +// ListDocuments returns metadata for every .md / .mdx file in the docs folder. +func (s *DocumentService) ListDocuments(ctx context.Context) ([]DocumentInfo, error) { + _ = ctx + entries, err := os.ReadDir(s.docsPath) + if err != nil { + return nil, fmt.Errorf("failed to read docs directory: %w", err) + } + + docs := make([]DocumentInfo, 0, len(entries)) + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + if !strings.HasSuffix(name, ".md") && !strings.HasSuffix(name, ".mdx") { + continue + } + + docType := "markdown" + if strings.HasSuffix(name, ".mdx") { + docType = "mdx" + } + + // Cheap frontmatter peek for the list view: read only if the first + // line is `---`, otherwise fall back to the filename. + title := strings.TrimSuffix(name, filepath.Ext(name)) + description := "" + if meta := peekFrontmatter(filepath.Join(s.docsPath, name)); meta != nil { + title = getStringFromMeta(meta, "title", title) + description = getStringFromMeta(meta, "description", "") + } + + docs = append(docs, DocumentInfo{ + Title: title, + Description: description, + Type: docType, + Path: name, + }) + } + + return docs, nil +} + +// DeleteDocument removes a document from disk. +func (s *DocumentService) DeleteDocument(ctx context.Context, filename string) error { + _ = ctx + filename = filepath.Base(filename) + filePath := filepath.Join(s.docsPath, filename) + if err := os.Remove(filePath); err != nil { + return fmt.Errorf("failed to delete document: %w", err) + } + return nil +} + +// splitFrontmatter separates a leading YAML `---` block from the body. +// Returns (nil, original) if no frontmatter is present. +func splitFrontmatter(content []byte) (map[string]interface{}, []byte) { + const delim = "---" + scanner := bufio.NewScanner(bytes.NewReader(content)) + scanner.Buffer(make([]byte, 1<<16), 1<<20) + + if !scanner.Scan() || strings.TrimSpace(scanner.Text()) != delim { + return nil, content + } + + var yamlBuf bytes.Buffer + closed := false + for scanner.Scan() { + line := scanner.Text() + if strings.TrimSpace(line) == delim { + closed = true + break + } + yamlBuf.WriteString(line) + yamlBuf.WriteByte('\n') + } + if !closed { + return nil, content + } + + var meta map[string]interface{} + if err := yaml.Unmarshal(yamlBuf.Bytes(), &meta); err != nil { + return nil, content + } + + var bodyBuf bytes.Buffer + for scanner.Scan() { + bodyBuf.WriteString(scanner.Text()) + bodyBuf.WriteByte('\n') + } + return meta, bodyBuf.Bytes() +} + +func peekFrontmatter(path string) map[string]interface{} { + data, err := os.ReadFile(path) + if err != nil { + return nil + } + meta, _ := splitFrontmatter(data) + return meta +} + +func getStringFromMeta(meta map[string]interface{}, key, defaultValue string) string { + if value, ok := meta[key]; ok { + if str, ok := value.(string); ok { + return str + } + } + return defaultValue +} diff --git a/internal/service/link.go b/internal/service/link.go index 93bbbd1..7295e68 100644 --- a/internal/service/link.go +++ b/internal/service/link.go @@ -8,6 +8,7 @@ import ( "time" "golinks/internal/domain" + "golinks/internal/logger" ) // ShortcutRepository interface for shortcut operations @@ -27,13 +28,16 @@ type QueryRepository interface { type LinkService struct { shortcutRepo ShortcutRepository queryRepo QueryRepository + logger *logger.Logger } // NewLinkService creates a new link service -func NewLinkService(shortcutRepo ShortcutRepository, queryRepo QueryRepository) *LinkService { +func NewLinkService(shortcutRepo ShortcutRepository, queryRepo QueryRepository, log *logger.Logger) *LinkService { + log.Info("Link service initialized") return &LinkService{ shortcutRepo: shortcutRepo, queryRepo: queryRepo, + logger: log, } } @@ -48,11 +52,12 @@ func (e InvalidQueryError) Error() string { // GetLink resolves a golink query to a URL func (s *LinkService) GetLink(ctx context.Context, word string, searchTerm string) (string, error) { - word = strings.TrimSpace(word) + s.logger.Debug("Processing golink query: '%s' (search: '%s')", word, searchTerm) shortcut, err := s.shortcutRepo.GetByWord(ctx, word) if err != nil { + s.logger.Error("Failed to get shortcut from repository: %v", err) return "", fmt.Errorf("failed to get shortcut: %w", err) } @@ -60,47 +65,53 @@ func (s *LinkService) GetLink(ctx context.Context, word string, searchTerm strin // Try splitting the word if it contains spaces if strings.Contains(word, " ") { newWord, newSearchTerm := moveLastWord(word, searchTerm) + s.logger.Debug("Splitting word '%s' -> '%s' and retrying", word, newWord) return s.GetLink(ctx, newWord, newSearchTerm) } + query := strings.Join([]string{word, searchTerm}, " ") + s.logger.Warn("No shortcut found for query: %s", query) return "", InvalidQueryError{ - Message: fmt.Sprintf("Unable to find link for query %s", strings.Join([]string{word, searchTerm}, " ")), + Message: fmt.Sprintf("Unable to find link for query %s", query), } } + s.logger.Info("Found shortcut: id=%d link='%s' user='%s'", shortcut.ID, shortcut.Link, shortcut.User) + // Log the query if err := s.queryRepo.Create(ctx, shortcut.ID); err != nil { - // Log error but don't fail the request - // In a production system, you might want to log this error - _ = err + s.logger.Error("Failed to log query usage for shortcut %d: %v", shortcut.ID, err) + // Don't fail the request for logging errors } // Handle different types of links if !isURL(shortcut.Link) { - // This is an alias, recurse + s.logger.Debug("Link is a keyword reference '%s', recursing", shortcut.Link) + // This is a keyword reference, recurse return s.GetLink(ctx, shortcut.Link, searchTerm) } // Process URL with search term substitution resultLink := processResultLink(shortcut.Link, searchTerm) + s.logger.Info("Link resolution successful: '%s' -> '%s'", word, resultLink) return resultLink, nil } // UpdateLink creates or updates a golink func (s *LinkService) UpdateLink(ctx context.Context, req domain.LinkRequest, userID string) error { + s.logger.Info("Processing link update: word='%s' link='%s' user='%s'", req.Word, req.Link, userID) // Validate the request if err := s.validateLinkRequest(ctx, req); err != nil { + s.logger.Warn("Link request validation failed: %v", err) return err } - // If the link is not a URL, validate it's a valid alias + // Validate that the link is a proper URL if !isURL(req.Link) { - _, err := s.GetLink(ctx, req.Link, "") - if err != nil { - return InvalidQueryError{ - Message: "The link target appears to neither be a URL, or a valid alias.", - } + s.logger.Warn("Invalid URL format: %s", req.Link) + return InvalidQueryError{ + Message: "URL must start with http:// or https://", } } @@ -112,32 +123,39 @@ func (s *LinkService) UpdateLink(ctx context.Context, req domain.LinkRequest, us } if err := s.shortcutRepo.Create(ctx, shortcut); err != nil { + s.logger.Error("Failed to create shortcut in repository: %v", err) return fmt.Errorf("failed to create shortcut: %w", err) } + s.logger.Info("Link update completed successfully: id=%d", shortcut.ID) return nil } // GetRecentQueries retrieves popular queries func (s *LinkService) GetRecentQueries(ctx context.Context) ([]domain.PopularQuery, error) { - return s.queryRepo.GetRecentQueries(ctx, 3, 20) + s.logger.Debug("Fetching recent queries (3 days, max 20 results)") + + queries, err := s.queryRepo.GetRecentQueries(ctx, 3, 20) + if err != nil { + s.logger.Error("Failed to get recent queries: %v", err) + return nil, err + } + + s.logger.Debug("Recent queries retrieved successfully: %d queries", len(queries)) + return queries, nil } -// GetAllKeywords retrieves all keywords with aliases +// GetAllKeywords retrieves all keywords func (s *LinkService) GetAllKeywords(ctx context.Context) ([]domain.KeywordInfo, error) { + s.logger.Debug("Fetching all keywords") + keywords, err := s.shortcutRepo.GetAllKeywords(ctx) if err != nil { + s.logger.Error("Failed to get all keywords: %v", err) return nil, err } - // Process aliases (simplified version - not implementing full recursive alias resolution for now) - for i := range keywords { - if !isURL(keywords[i].Link) { - keywords[i].Aliases = keywords[i].Link - } - } - - // Filter to only return URLs (not aliases) + // Filter to only return URLs var result []domain.KeywordInfo for _, keyword := range keywords { if isURL(keyword.Link) { @@ -145,6 +163,7 @@ func (s *LinkService) GetAllKeywords(ctx context.Context) ([]domain.KeywordInfo, } } + s.logger.Debug("Keywords retrieved successfully: %d total, %d URLs", len(keywords), len(result)) return result, nil } diff --git a/internal/service/link_test.go b/internal/service/link_test.go index d665d90..4a8d034 100644 --- a/internal/service/link_test.go +++ b/internal/service/link_test.go @@ -6,6 +6,7 @@ import ( "time" "golinks/internal/domain" + "golinks/internal/logger" ) // Mock repositories for testing @@ -33,13 +34,11 @@ func (m *mockShortcutRepository) Create(ctx context.Context, shortcut *domain.Sh func (m *mockShortcutRepository) GetAllKeywords(ctx context.Context) ([]domain.KeywordInfo, error) { var keywords []domain.KeywordInfo for word, shortcut := range m.shortcuts { - if isURL(shortcut.Link) { - keywords = append(keywords, domain.KeywordInfo{ - Word: word, - Link: shortcut.Link, - CreatedAt: shortcut.CreatedAt, - }) - } + keywords = append(keywords, domain.KeywordInfo{ + Word: word, + Link: shortcut.Link, + CreatedAt: shortcut.CreatedAt, + }) } return keywords, nil } @@ -109,7 +108,7 @@ func TestLinkService_GetLink(t *testing.T) { wantErr: false, }, { - name: "alias redirect", + name: "keyword reference redirect", shortcuts: map[string]*domain.Shortcut{ "d": { ID: 1, @@ -158,7 +157,8 @@ func TestLinkService_GetLink(t *testing.T) { t.Run(tt.name, func(t *testing.T) { shortcutRepo := &mockShortcutRepository{shortcuts: tt.shortcuts} queryRepo := &mockQueryRepository{} - service := NewLinkService(shortcutRepo, queryRepo) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + service := NewLinkService(shortcutRepo, queryRepo, mockLogger) got, err := service.GetLink(context.Background(), tt.word, tt.searchTerm) @@ -192,23 +192,6 @@ func TestLinkService_UpdateLink(t *testing.T) { userID: "testuser", wantErr: false, }, - { - name: "valid alias", - shortcuts: map[string]*domain.Shortcut{ - "docs": { - ID: 1, - Word: "docs", - Link: "https://docs.example.com", - User: "testuser", - }, - }, - request: domain.LinkRequest{ - Word: "d", - Link: "docs", - }, - userID: "testuser", - wantErr: false, - }, { name: "empty word", shortcuts: map[string]*domain.Shortcut{}, @@ -240,11 +223,11 @@ func TestLinkService_UpdateLink(t *testing.T) { wantErr: true, }, { - name: "invalid alias target", + name: "invalid URL format", shortcuts: map[string]*domain.Shortcut{}, request: domain.LinkRequest{ - Word: "d", - Link: "nonexistent", + Word: "docs", + Link: "example.com", // Missing http:// or https:// }, userID: "testuser", wantErr: true, @@ -255,7 +238,8 @@ func TestLinkService_UpdateLink(t *testing.T) { t.Run(tt.name, func(t *testing.T) { shortcutRepo := &mockShortcutRepository{shortcuts: tt.shortcuts} queryRepo := &mockQueryRepository{} - service := NewLinkService(shortcutRepo, queryRepo) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + service := NewLinkService(shortcutRepo, queryRepo, mockLogger) err := service.UpdateLink(context.Background(), tt.request, tt.userID) @@ -269,7 +253,8 @@ func TestLinkService_UpdateLink(t *testing.T) { func TestLinkService_GetRecentQueries(t *testing.T) { shortcutRepo := &mockShortcutRepository{shortcuts: map[string]*domain.Shortcut{}} queryRepo := &mockQueryRepository{} - service := NewLinkService(shortcutRepo, queryRepo) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + service := NewLinkService(shortcutRepo, queryRepo, mockLogger) queries, err := service.GetRecentQueries(context.Background()) @@ -296,10 +281,10 @@ func TestLinkService_GetAllKeywords(t *testing.T) { User: "testuser", CreatedAt: time.Now(), }, - "d": { + "github": { ID: 2, - Word: "d", - Link: "docs", // This is an alias, should be filtered out + Word: "github", + Link: "https://github.com", User: "testuser", CreatedAt: time.Now(), }, @@ -307,7 +292,8 @@ func TestLinkService_GetAllKeywords(t *testing.T) { shortcutRepo := &mockShortcutRepository{shortcuts: shortcuts} queryRepo := &mockQueryRepository{} - service := NewLinkService(shortcutRepo, queryRepo) + mockLogger := logger.New(logger.Config{Level: "debug", Format: "text"}) + service := NewLinkService(shortcutRepo, queryRepo, mockLogger) keywords, err := service.GetAllKeywords(context.Background()) @@ -315,13 +301,23 @@ func TestLinkService_GetAllKeywords(t *testing.T) { t.Errorf("LinkService.GetAllKeywords() error = %v", err) } - // Should only return URLs, not aliases - if len(keywords) != 1 { - t.Errorf("LinkService.GetAllKeywords() expected 1 keyword, got %d", len(keywords)) + // Should return only URLs + if len(keywords) != 2 { + t.Errorf("LinkService.GetAllKeywords() expected 2 keywords, got %d", len(keywords)) + } + + // Check that we have both keywords + keywordMap := make(map[string]bool) + for _, keyword := range keywords { + keywordMap[keyword.Word] = true + } + + if !keywordMap["docs"] { + t.Error("LinkService.GetAllKeywords() missing 'docs' keyword") } - if keywords[0].Word != "docs" { - t.Errorf("LinkService.GetAllKeywords() expected 'docs', got %s", keywords[0].Word) + if !keywordMap["github"] { + t.Error("LinkService.GetAllKeywords() missing 'github' keyword") } } diff --git a/web/frontend/components.json b/web/frontend/components.json new file mode 100644 index 0000000..782a943 --- /dev/null +++ b/web/frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/web/frontend/dist/index.html b/web/frontend/dist/index.html new file mode 100644 index 0000000..884cc99 --- /dev/null +++ b/web/frontend/dist/index.html @@ -0,0 +1,14 @@ + + + + + + + golinks + + + + +
+ + diff --git a/web/frontend/embed.go b/web/frontend/embed.go new file mode 100644 index 0000000..a2831dd --- /dev/null +++ b/web/frontend/embed.go @@ -0,0 +1,87 @@ +// Package frontend embeds the built Vite/React SPA and serves it. +// +// The go:embed directive below pulls every file under web/frontend/dist/ into +// the binary at compile time. For SPA routing to work we serve concrete files +// when they exist and fall back to index.html for every other GET — so a hard +// refresh on /docs/sample.md still lands on the React router. +// +// If the dist directory is empty (e.g. `go build` was run without `npm run +// build` first), Handler degrades to a helpful 503 rather than a crash so the +// caller can diagnose the missing build step. +package frontend + +import ( + "embed" + "fmt" + "io" + "io/fs" + "net/http" + "strings" +) + +//go:embed all:dist +var distFS embed.FS + +// Handler returns an http.Handler that serves the embedded SPA. +// Requests starting with any of `reservedPrefixes` are rejected with 404 so +// they can be handled by other routes; in practice we only call Handler on +// the catch-all route, so the reservation is belt-and-braces. +func Handler(reservedPrefixes ...string) http.Handler { + sub, err := fs.Sub(distFS, "dist") + if err != nil { + return brokenHandler(fmt.Errorf("failed to locate embedded frontend: %w", err)) + } + + if _, err := fs.Stat(sub, "index.html"); err != nil { + return brokenHandler(fmt.Errorf( + "embedded frontend is empty — run `npm run build` in web/frontend/ before `go build`: %w", + err, + )) + } + + fileServer := http.FileServer(http.FS(sub)) + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodHead { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + reqPath := strings.TrimPrefix(r.URL.Path, "/") + for _, p := range reservedPrefixes { + if strings.HasPrefix(reqPath, strings.TrimPrefix(p, "/")) { + http.NotFound(w, r) + return + } + } + + if reqPath != "" { + if f, err := sub.Open(reqPath); err == nil { + f.Close() + fileServer.ServeHTTP(w, r) + return + } + } + + serveIndex(w, sub) + }) +} + +func serveIndex(w http.ResponseWriter, sub fs.FS) { + f, err := sub.Open("index.html") + if err != nil { + http.Error(w, "index.html not found", http.StatusInternalServerError) + return + } + defer f.Close() + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Cache-Control", "no-cache") + _, _ = io.Copy(w, f) +} + +func brokenHandler(err error) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + }) +} diff --git a/web/frontend/index.html b/web/frontend/index.html new file mode 100644 index 0000000..065dabf --- /dev/null +++ b/web/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + golinks + + +
+ + + diff --git a/web/frontend/package-lock.json b/web/frontend/package-lock.json new file mode 100644 index 0000000..ffc1113 --- /dev/null +++ b/web/frontend/package-lock.json @@ -0,0 +1,5683 @@ +{ + "name": "golinks-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "golinks-frontend", + "version": "0.1.0", + "dependencies": { + "@fontsource/inter": "^5.2.5", + "@fontsource/jetbrains-mono": "^5.2.5", + "@hookform/resolvers": "^4.1.3", + "@mdx-js/mdx": "^3.1.0", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", + "@tailwindcss/typography": "^0.5.16", + "@tanstack/react-query": "^5.66.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "highlight.js": "^11.11.1", + "lucide-react": "^0.475.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.54.2", + "react-router-dom": "^6.28.1", + "rehype-highlight": "^7.0.2", + "remark-gfm": "^4.0.1", + "sonner": "^2.0.1", + "tailwind-merge": "^3.0.2", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/mdx": "^2.0.13", + "@types/node": "^22.19.17", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.2", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3", + "vite": "^6.1.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fontsource/inter": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz", + "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/jetbrains-mono": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", + "integrity": "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@hookform/resolvers": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz", + "integrity": "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.99.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.99.2.tgz", + "integrity": "sha512-1HunU0bXVsR1ZJMZbcOPE6VtaBJxsW809RE9xPe4Gz7MlB0GWwQvuTPhMoEmQ/hIzFKJ/DWAuttIe7BOaWx0tA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.99.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.99.2.tgz", + "integrity": "sha512-vM91UEe45QUS9ED6OklsVL15i8qKcRqNwpWzPTVWvRPRSEgDudDgHpvyTjcdlwHcrKNa80T+xXYcchT2noPnZA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.99.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/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz", + "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001790", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", + "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.343", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.343.tgz", + "integrity": "sha512-YHnQ3MXI08icvL9ZKnEBy05F2EQ8ob01UaMOuMbM8l+4UcAq6MPPbBTJBbsBUg3H8JeZNt+O4fjsoWth3p6IFg==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/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/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lowlight": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", + "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "highlight.js": "~11.11.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.475.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.475.0.tgz", + "integrity": "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-hook-form": { + "version": "7.73.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.73.1.tgz", + "integrity": "sha512-VAfVYOPcx3piiEVQy95vyFmBwbVUsP/AUIN+mpFG8h11yshDd444nn0VyfaGWSRnhOLVgiDu7HIuBtAIzxn9dA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-highlight": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz", + "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-text": "^4.0.0", + "lowlight": "^3.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/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/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "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/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/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/web/frontend/package.json b/web/frontend/package.json new file mode 100644 index 0000000..c580306 --- /dev/null +++ b/web/frontend/package.json @@ -0,0 +1,50 @@ +{ + "name": "golinks-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "typecheck": "tsc -b --noEmit" + }, + "dependencies": { + "@fontsource/inter": "^5.2.5", + "@fontsource/jetbrains-mono": "^5.2.5", + "@hookform/resolvers": "^4.1.3", + "@mdx-js/mdx": "^3.1.0", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", + "@tailwindcss/typography": "^0.5.16", + "@tanstack/react-query": "^5.66.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "highlight.js": "^11.11.1", + "lucide-react": "^0.475.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.54.2", + "react-router-dom": "^6.28.1", + "rehype-highlight": "^7.0.2", + "remark-gfm": "^4.0.1", + "sonner": "^2.0.1", + "tailwind-merge": "^3.0.2", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/mdx": "^2.0.13", + "@types/node": "^22.19.17", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.2", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3", + "vite": "^6.1.0" + } +} diff --git a/web/frontend/postcss.config.js b/web/frontend/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/web/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/web/static/favicon.ico b/web/frontend/public/favicon.ico similarity index 100% rename from web/static/favicon.ico rename to web/frontend/public/favicon.ico diff --git a/web/frontend/src/App.tsx b/web/frontend/src/App.tsx new file mode 100644 index 0000000..a85c088 --- /dev/null +++ b/web/frontend/src/App.tsx @@ -0,0 +1,27 @@ +import { Route, Routes } from "react-router-dom"; + +import { Navbar } from "@/components/Navbar"; +import { HomePage } from "@/pages/Home"; +import { SetupPage } from "@/pages/Setup"; +import { DocsListPage } from "@/pages/DocsList"; +import { DocPage } from "@/pages/Doc"; +import { NotFoundPage } from "@/pages/NotFound"; + +export default function App() { + return ( +
+ +
+ + } /> + {/* Legacy route from the template era. */} + } /> + } /> + } /> + } /> + } /> + +
+
+ ); +} diff --git a/web/frontend/src/components/DocUploader.tsx b/web/frontend/src/components/DocUploader.tsx new file mode 100644 index 0000000..7299953 --- /dev/null +++ b/web/frontend/src/components/DocUploader.tsx @@ -0,0 +1,48 @@ +import * as React from "react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { Upload } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { api, ApiError } from "@/lib/api"; + +export function DocUploader() { + const inputRef = React.useRef(null); + const qc = useQueryClient(); + + const mutation = useMutation({ + mutationFn: api.uploadDoc, + onSuccess: (data) => { + toast.success(`Uploaded ${data.filename}`); + qc.invalidateQueries({ queryKey: ["docs"] }); + }, + onError: (err: unknown) => { + const msg = err instanceof ApiError ? err.message : "Upload failed"; + toast.error(msg); + }, + }); + + return ( + <> + { + const file = e.target.files?.[0]; + if (file) mutation.mutate(file); + e.target.value = ""; + }} + /> + + + ); +} diff --git a/web/frontend/src/components/KeywordTable.tsx b/web/frontend/src/components/KeywordTable.tsx new file mode 100644 index 0000000..b005682 --- /dev/null +++ b/web/frontend/src/components/KeywordTable.tsx @@ -0,0 +1,65 @@ +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import type { KeywordInfo } from "@/lib/api"; + +type Props = { keywords: KeywordInfo[] }; + +function formatDate(iso: string) { + if (!iso) return ""; + try { + return new Date(iso).toISOString().slice(0, 10); + } catch { + return iso; + } +} + +export function KeywordTable({ keywords }: Props) { + if (keywords.length === 0) { + return ( +

+ No keywords yet — add your first one above. +

+ ); + } + + return ( + + + + Keyword + URL + Created + + + + {keywords.map((k) => ( + + + + {k.word} + + + + {k.link.startsWith("http://") || k.link.startsWith("https://") ? ( + + {k.link} + + ) : ( + k.link + )} + + + {formatDate(k.created_at)} + + + ))} + +
+ ); +} diff --git a/web/frontend/src/components/LinkForm.tsx b/web/frontend/src/components/LinkForm.tsx new file mode 100644 index 0000000..b1b061a --- /dev/null +++ b/web/frontend/src/components/LinkForm.tsx @@ -0,0 +1,95 @@ +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { api, ApiError } from "@/lib/api"; + +const schema = z.object({ + word: z + .string() + .trim() + .min(1, "Please enter a keyword.") + .refine((v) => !v.endsWith("/"), "Keywords ending in '/' are not supported."), + link: z.string().trim().min(1, "Please enter a URL."), +}); + +type FormValues = z.infer; + +export function LinkForm() { + const qc = useQueryClient(); + const { + register, + handleSubmit, + reset, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { word: "", link: "" }, + }); + + const mutation = useMutation({ + mutationFn: api.createLink, + onSuccess: (_, vars) => { + toast.success(`Added keyword "${vars.word}"`); + reset(); + qc.invalidateQueries({ queryKey: ["links"] }); + }, + onError: (err: unknown) => { + const msg = err instanceof ApiError ? err.message : "Failed to add link"; + toast.error(msg); + }, + }); + + return ( +
mutation.mutate(values))} + className="space-y-3" + noValidate + > +
+
+ + +
+
+ + +
+ +
+ {(errors.word || errors.link) && ( +

+ {errors.word?.message ?? errors.link?.message} +

+ )} +
+ ); +} diff --git a/web/frontend/src/components/MDXRenderer.tsx b/web/frontend/src/components/MDXRenderer.tsx new file mode 100644 index 0000000..562cc71 --- /dev/null +++ b/web/frontend/src/components/MDXRenderer.tsx @@ -0,0 +1,60 @@ +import * as React from "react"; + +import { compileMDX } from "@/lib/mdx"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; + +type Props = { source: string }; + +export function MDXRenderer({ source }: Props) { + const [Content, setContent] = React.useState(null); + const [error, setError] = React.useState(null); + + React.useEffect(() => { + let cancelled = false; + setContent(null); + setError(null); + + compileMDX(source) + .then((mod) => { + if (cancelled) return; + setContent(() => mod.default); + }) + .catch((err: unknown) => { + if (cancelled) return; + setError(err instanceof Error ? err.message : String(err)); + }); + + return () => { + cancelled = true; + }; + }, [source]); + + if (error) { + return ( + + Failed to render document + +
{error}
+
+
+ ); + } + + if (!Content) { + return ( +
+ + + + +
+ ); + } + + return ( +
+ +
+ ); +} diff --git a/web/frontend/src/components/Navbar.tsx b/web/frontend/src/components/Navbar.tsx new file mode 100644 index 0000000..8434d62 --- /dev/null +++ b/web/frontend/src/components/Navbar.tsx @@ -0,0 +1,40 @@ +import { NavLink } from "react-router-dom"; + +import { cn } from "@/lib/utils"; + +const links = [ + { to: "/", label: "Home", end: true }, + { to: "/setup", label: "Setup" }, + { to: "/docs", label: "Docs" }, +]; + +export function Navbar() { + return ( + + ); +} diff --git a/web/frontend/src/components/RecentQueries.tsx b/web/frontend/src/components/RecentQueries.tsx new file mode 100644 index 0000000..deca34c --- /dev/null +++ b/web/frontend/src/components/RecentQueries.tsx @@ -0,0 +1,24 @@ +import type { PopularQuery } from "@/lib/api"; + +type Props = { queries: PopularQuery[] }; + +export function RecentQueries({ queries }: Props) { + if (queries.length === 0) { + return null; + } + + return ( +
+

Recent queries

+
    + {queries.map((q) => ( +
  • + ×{q.count} + {q.word} + → {q.link} +
  • + ))} +
+
+ ); +} diff --git a/web/frontend/src/components/ui/alert.tsx b/web/frontend/src/components/ui/alert.tsx new file mode 100644 index 0000000..f349aaa --- /dev/null +++ b/web/frontend/src/components/ui/alert.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const alertVariants = cva( + "relative w-full rounded-md border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "border-destructive/40 bg-destructive/10 text-destructive [&>svg]:text-destructive", + success: + "border-success/40 bg-success/10 text-success [&>svg]:text-success", + info: "border-accent/40 bg-accent/10 text-accent [&>svg]:text-accent", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = "AlertDescription"; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/web/frontend/src/components/ui/button.tsx b/web/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..23da2da --- /dev/null +++ b/web/frontend/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-secondary hover:text-secondary-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-secondary hover:text-secondary-foreground", + link: "text-accent underline-offset-4 hover:underline hover:text-primary", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-6", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + }, +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/web/frontend/src/components/ui/card.tsx b/web/frontend/src/components/ui/card.tsx new file mode 100644 index 0000000..4123a5b --- /dev/null +++ b/web/frontend/src/components/ui/card.tsx @@ -0,0 +1,62 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardFooter.displayName = "CardFooter"; + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/web/frontend/src/components/ui/dialog.tsx b/web/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..58acf02 --- /dev/null +++ b/web/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,99 @@ +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Dialog = DialogPrimitive.Root; +const DialogTrigger = DialogPrimitive.Trigger; +const DialogPortal = DialogPrimitive.Portal; +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/web/frontend/src/components/ui/input.tsx b/web/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..36c4a33 --- /dev/null +++ b/web/frontend/src/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; + +export { Input }; diff --git a/web/frontend/src/components/ui/label.tsx b/web/frontend/src/components/ui/label.tsx new file mode 100644 index 0000000..530165c --- /dev/null +++ b/web/frontend/src/components/ui/label.tsx @@ -0,0 +1,19 @@ +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", +); + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/web/frontend/src/components/ui/skeleton.tsx b/web/frontend/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..de994ae --- /dev/null +++ b/web/frontend/src/components/ui/skeleton.tsx @@ -0,0 +1,7 @@ +import { cn } from "@/lib/utils"; + +function Skeleton({ className, ...props }: React.HTMLAttributes) { + return
; +} + +export { Skeleton }; diff --git a/web/frontend/src/components/ui/sonner.tsx b/web/frontend/src/components/ui/sonner.tsx new file mode 100644 index 0000000..75b9e41 --- /dev/null +++ b/web/frontend/src/components/ui/sonner.tsx @@ -0,0 +1,22 @@ +import { Toaster as Sonner, type ToasterProps } from "sonner"; + +const Toaster = ({ ...props }: ToasterProps) => { + return ( + + ); +}; + +export { Toaster }; diff --git a/web/frontend/src/components/ui/table.tsx b/web/frontend/src/components/ui/table.tsx new file mode 100644 index 0000000..d6f7c46 --- /dev/null +++ b/web/frontend/src/components/ui/table.tsx @@ -0,0 +1,114 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Table = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ + + ), +); +Table.displayName = "Table"; + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableHeader.displayName = "TableHeader"; + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = "TableBody"; + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className, + )} + {...props} + /> +)); +TableFooter.displayName = "TableFooter"; + +const TableRow = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ), +); +TableRow.displayName = "TableRow"; + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableHead.displayName = "TableHead"; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableCell.displayName = "TableCell"; + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableCaption.displayName = "TableCaption"; + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}; diff --git a/web/frontend/src/components/ui/tabs.tsx b/web/frontend/src/components/ui/tabs.tsx new file mode 100644 index 0000000..5e356fa --- /dev/null +++ b/web/frontend/src/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; + +import { cn } from "@/lib/utils"; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/web/frontend/src/index.css b/web/frontend/src/index.css new file mode 100644 index 0000000..a5ae1cd --- /dev/null +++ b/web/frontend/src/index.css @@ -0,0 +1,90 @@ +@import "@fontsource/inter/300.css"; +@import "@fontsource/inter/400.css"; +@import "@fontsource/inter/500.css"; +@import "@fontsource/inter/600.css"; +@import "@fontsource/jetbrains-mono/400.css"; +@import "@fontsource/jetbrains-mono/500.css"; +@import "highlight.js/styles/github.css"; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + /* Rams-inspired palette ported to shadcn HSL tokens. + Originals kept as reference in the comments. */ + + --background: 0 0% 98%; /* rams-off-white #FAFAFA */ + --foreground: 0 0% 20%; /* rams-charcoal #333333 */ + + --card: 0 0% 100%; /* rams-white */ + --card-foreground: 0 0% 10%; /* rams-black */ + + --popover: 0 0% 100%; + --popover-foreground: 0 0% 10%; + + --primary: 14 100% 60%; /* rams-orange #FF6B35 — Braun accent */ + --primary-foreground: 0 0% 100%; + + --secondary: 0 0% 90%; /* rams-light-grey */ + --secondary-foreground: 0 0% 20%; + + --muted: 0 0% 96%; + --muted-foreground: 0 0% 40%; /* rams-dark-grey */ + + --accent: 221 83% 53%; /* rams-blue #2563EB — for links/focus */ + --accent-foreground: 0 0% 100%; + + --destructive: 0 72% 51%; /* rams-red #DC2626 */ + --destructive-foreground: 0 0% 100%; + + --success: 162 94% 30%; /* rams-green #059669 */ + --success-foreground: 0 0% 100%; + + --border: 0 0% 90%; /* rams-light-grey */ + --input: 0 0% 80%; /* rams-medium-grey */ + --ring: 14 100% 60%; /* rams-orange */ + + --radius: 0.25rem; /* 4px — rams-radius-md */ + } +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground font-sans antialiased; + font-feature-settings: "rlig" 1, "calt" 1; + } + + /* Rams-flavoured prose overrides for MDX-rendered docs. + We keep the accent orange for link hover and use the mono font for code. */ + .prose :where(a):not(:where([class~="not-prose"] *)) { + @apply text-accent no-underline font-medium; + } + .prose :where(a):not(:where([class~="not-prose"] *)):hover { + @apply text-primary underline; + } + .prose :where(code):not(:where([class~="not-prose"] *)) { + @apply font-mono text-sm bg-secondary border border-input rounded-sm px-1 py-0.5; + } + .prose :where(code):not(:where([class~="not-prose"] *))::before, + .prose :where(code):not(:where([class~="not-prose"] *))::after { + content: none; + } + .prose :where(pre):not(:where([class~="not-prose"] *)) { + @apply font-mono bg-card border border-border rounded-md; + } + .prose :where(pre code):not(:where([class~="not-prose"] *)) { + @apply bg-transparent border-0 p-0 text-sm; + } + .prose :where(h1, h2, h3, h4):not(:where([class~="not-prose"] *)) { + @apply font-medium tracking-tight text-foreground; + } + .prose :where(h1):not(:where([class~="not-prose"] *)) { + @apply font-light; + } +} diff --git a/web/frontend/src/lib/api.ts b/web/frontend/src/lib/api.ts new file mode 100644 index 0000000..584a3aa --- /dev/null +++ b/web/frontend/src/lib/api.ts @@ -0,0 +1,96 @@ +export type KeywordInfo = { + word: string; + link: string; + created_at: string; +}; + +export type PopularQuery = { + count: number; + word: string; + link: string; +}; + +export type LinksResponse = { + keywords: KeywordInfo[]; + recent_queries: PopularQuery[]; + base_url: string; +}; + +export type DocumentInfo = { + title: string; + description?: string; + type: "markdown" | "mdx"; + path: string; +}; + +export type DocumentSource = { + source: string; + type: "markdown" | "mdx"; + metadata: DocumentInfo; +}; + +export type LinkInput = { + word: string; + link: string; +}; + +class ApiError extends Error { + constructor( + message: string, + public status: number, + ) { + super(message); + this.name = "ApiError"; + } +} + +async function request(path: string, init?: RequestInit): Promise { + const res = await fetch(path, { + headers: { + "Content-Type": "application/json", + ...(init?.headers ?? {}), + }, + ...init, + }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new ApiError(text || res.statusText, res.status); + } + + // DELETE can return empty + const ct = res.headers.get("content-type") ?? ""; + if (!ct.includes("application/json")) { + return undefined as T; + } + return (await res.json()) as T; +} + +export const api = { + listLinks: () => request("/api/links"), + createLink: (input: LinkInput) => + request<{ success: true }>("/api/links", { + method: "POST", + body: JSON.stringify(input), + }), + listDocs: () => request<{ documents: DocumentInfo[] }>("/api/docs"), + getDoc: (filename: string) => + request(`/api/docs/${encodeURIComponent(filename)}`), + uploadDoc: async (file: File) => { + const form = new FormData(); + form.append("file", file); + const res = await fetch("/api/docs", { method: "POST", body: form }); + if (!res.ok) throw new ApiError(await res.text(), res.status); + return (await res.json()) as { + success: true; + filename: string; + url: string; + }; + }, + deleteDoc: (filename: string) => + request<{ success: true }>(`/api/docs/${encodeURIComponent(filename)}`, { + method: "DELETE", + }), +}; + +export { ApiError }; diff --git a/web/frontend/src/lib/mdx.tsx b/web/frontend/src/lib/mdx.tsx new file mode 100644 index 0000000..d2f944b --- /dev/null +++ b/web/frontend/src/lib/mdx.tsx @@ -0,0 +1,55 @@ +import * as React from "react"; +import * as runtime from "react/jsx-runtime"; +import { evaluate } from "@mdx-js/mdx"; + +type MDXModule = { default: React.ComponentType }; +import remarkGfm from "remark-gfm"; +import rehypeHighlight from "rehype-highlight"; + +import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { + Table, + TableHeader, + TableBody, + TableRow, + TableHead, + TableCell, +} from "@/components/ui/table"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; + +// Components exposed to MDX. Authors can write ... +// directly inside their .mdx files and it will render as a shadcn Alert. +const mdxComponents = { + Alert, + AlertTitle, + AlertDescription, + Button, + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + Tabs, + TabsList, + TabsTrigger, + TabsContent, + // Wire shadcn Table primitives to plain markdown tables so GFM tables pick up the styling. + table: (props: React.HTMLAttributes) => , + thead: (props: React.HTMLAttributes) => , + tbody: (props: React.HTMLAttributes) => , + tr: (props: React.HTMLAttributes) => , + th: (props: React.ThHTMLAttributes) => , + td: (props: React.TdHTMLAttributes) => , +}; + +export async function compileMDX(source: string): Promise { + return evaluate(source, { + ...(runtime as typeof runtime & { Fragment: React.ComponentType }), + baseUrl: import.meta.url, + remarkPlugins: [remarkGfm], + rehypePlugins: [rehypeHighlight], + useMDXComponents: () => mdxComponents, + }); +} diff --git a/web/frontend/src/lib/utils.ts b/web/frontend/src/lib/utils.ts new file mode 100644 index 0000000..a5ef193 --- /dev/null +++ b/web/frontend/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/web/frontend/src/main.tsx b/web/frontend/src/main.tsx new file mode 100644 index 0000000..516dca1 --- /dev/null +++ b/web/frontend/src/main.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "@/components/ui/sonner"; + +import App from "./App"; +import "./index.css"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { staleTime: 30_000, refetchOnWindowFocus: false }, + }, +}); + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + + + + , +); diff --git a/web/frontend/src/pages/Doc.tsx b/web/frontend/src/pages/Doc.tsx new file mode 100644 index 0000000..99792fd --- /dev/null +++ b/web/frontend/src/pages/Doc.tsx @@ -0,0 +1,61 @@ +import { Link, useParams } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; +import { ArrowLeft } from "lucide-react"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { MDXRenderer } from "@/components/MDXRenderer"; +import { api } from "@/lib/api"; + +export function DocPage() { + const { filename = "" } = useParams(); + const { data, isLoading, error } = useQuery({ + queryKey: ["doc", filename], + queryFn: () => api.getDoc(filename), + enabled: Boolean(filename), + }); + + return ( +
+ + + {error && ( + + Document not found + + {error instanceof Error ? error.message : String(error)} + + + )} + + {isLoading && ( +
+ + + +
+ )} + + {data && ( + <> +
+
+ {data.metadata.type} +
+

{data.metadata.title}

+ {data.metadata.description && ( +

{data.metadata.description}

+ )} +
+ + + )} +
+ ); +} diff --git a/web/frontend/src/pages/DocsList.tsx b/web/frontend/src/pages/DocsList.tsx new file mode 100644 index 0000000..39bc315 --- /dev/null +++ b/web/frontend/src/pages/DocsList.tsx @@ -0,0 +1,104 @@ +import { Link } from "react-router-dom"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { FileText, Trash2 } from "lucide-react"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { DocUploader } from "@/components/DocUploader"; +import { api, ApiError } from "@/lib/api"; + +export function DocsListPage() { + const qc = useQueryClient(); + const { data, isLoading, error } = useQuery({ + queryKey: ["docs"], + queryFn: api.listDocs, + }); + + const deleteMutation = useMutation({ + mutationFn: api.deleteDoc, + onSuccess: () => { + toast.success("Document deleted"); + qc.invalidateQueries({ queryKey: ["docs"] }); + }, + onError: (err: unknown) => { + toast.error(err instanceof ApiError ? err.message : "Delete failed"); + }, + }); + + return ( +
+
+
+

Docs

+

+ Markdown and MDX documents rendered with shadcn primitives. +

+
+ +
+ + {error && ( + + Couldn't load documents + + {error instanceof Error ? error.message : String(error)} + + + )} + + {isLoading ? ( +
+ + +
+ ) : (data?.documents?.length ?? 0) === 0 ? ( + + + + No documents yet. Upload a .md or{" "} + .mdx file to get started. + + + ) : ( +
    + {data!.documents.map((doc) => ( +
  • + + + + +
    + {doc.title} + {doc.path} +
    + + {doc.type} + + + +
    +
    +
  • + ))} +
+ )} +
+ ); +} diff --git a/web/frontend/src/pages/Home.tsx b/web/frontend/src/pages/Home.tsx new file mode 100644 index 0000000..d71a71e --- /dev/null +++ b/web/frontend/src/pages/Home.tsx @@ -0,0 +1,93 @@ +import { useEffect } from "react"; +import { useSearchParams } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; +import { toast } from "sonner"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Skeleton } from "@/components/ui/skeleton"; +import { KeywordTable } from "@/components/KeywordTable"; +import { LinkForm } from "@/components/LinkForm"; +import { RecentQueries } from "@/components/RecentQueries"; +import { api } from "@/lib/api"; + +export function HomePage() { + const [params, setParams] = useSearchParams(); + const missing = params.get("missing"); + + const { data, isLoading, error } = useQuery({ + queryKey: ["links"], + queryFn: api.listLinks, + }); + + // One-shot toast when the golink resolver bounces the user back here. + useEffect(() => { + if (missing) { + toast.error(`No shortcut found for "${missing}"`); + params.delete("missing"); + setParams(params, { replace: true }); + } + }, [missing, params, setParams]); + + return ( +
+
+

+ golinks +

+

+ Memorable shortcuts for long URLs. Type go <keyword> in your browser address bar once + you've finished the setup. +

+ {data?.base_url && ( +

+ Your search engine should read:{" "} + + {data.base_url}/query/%s + +

+ )} +
+ +
+

+ + Add a new keyword +

+ +

+ Use {"{*}"} in the URL + for a variable. Example: https://github.com/search?q={"{*}"}, then go github claude. +

+
+ + {error && ( + + Couldn't load keywords + {error instanceof Error ? error.message : String(error)} + + )} + +
+

+ + All keywords +

+ {isLoading ? ( +
+ + + +
+ ) : ( + + )} +
+ + {data?.recent_queries && data.recent_queries.length > 0 && ( +
+ +
+ )} +
+ ); +} diff --git a/web/frontend/src/pages/NotFound.tsx b/web/frontend/src/pages/NotFound.tsx new file mode 100644 index 0000000..d1c7e9e --- /dev/null +++ b/web/frontend/src/pages/NotFound.tsx @@ -0,0 +1,49 @@ +import { Link, useLocation } from "react-router-dom"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; + +export function NotFoundPage() { + const { pathname } = useLocation(); + + return ( +
+
+
404
+

Page not found

+

+ Couldn't find{" "} + + {pathname} + + . +

+
+ + +
+ + +

If you were trying to use a golink:

+
    +
  • + Check the keyword is typed correctly — try the{" "} + + keyword list + + . +
  • +
  • + Add it on the home page if it doesn't exist yet. +
  • +
+
+
+
+
+ ); +} diff --git a/web/frontend/src/pages/Setup.tsx b/web/frontend/src/pages/Setup.tsx new file mode 100644 index 0000000..c3009da --- /dev/null +++ b/web/frontend/src/pages/Setup.tsx @@ -0,0 +1,164 @@ +import { useQuery } from "@tanstack/react-query"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { api } from "@/lib/api"; + +function BaseUrlCode() { + const { data } = useQuery({ queryKey: ["links"], queryFn: api.listLinks }); + return ( + + {(data?.base_url ?? "") + "/query/%s"} + + ); +} + +export function SetupPage() { + return ( +
+
+

Browser setup

+

+ Configure your browser to use GoLinks as a search engine so typing{" "} + go <keyword> in the + address bar redirects to the matching URL. +

+
+ + + + Chrome / Edge + Firefox + Safari + + +
    +
  1. Open Chrome/Edge settings.
  2. +
  3. + Go to Search engine →{" "} + Manage search engines and site search. +
  4. +
  5. + Click Add next to "Site search". +
  6. +
  7. + Fill in: +
      +
    • + Name: GoLinks +
    • +
    • + Shortcut:{" "} + go +
    • +
    • + URL: +
    • +
    +
  8. +
  9. + Click Add. +
  10. +
+
+ +
    +
  1. Open Bookmarks → Manage Bookmarks.
  2. +
  3. + Create a new bookmark with: +
      +
    • + Name: GoLinks +
    • +
    • + Location: +
    • +
    • + Keyword:{" "} + go +
    • +
    +
  4. +
  5. Save.
  6. +
+
+ +

+ Safari doesn't support custom search engines natively. Options: +

+
    +
  • + Bookmark / for quick + access. +
  • +
  • + Use an extension like Keyword Search. +
  • +
+
+
+ + + Pro tip + + Once configured, just type go keyword in your address bar. + + + +
+

+ + Usage examples +

+
+ + + Command + Description + Notes + + + + + + go docs + + Navigate to a fixed URL. + + If docs points to{" "} + https://docs.example.com. + + + + + go jira 123 + + Navigate with a parameter. + + Works if jira contains{" "} + {"{*}"}. + + + + + go github myrepo + + Dynamic search. + + Same {"{*}"} substitution. + + + +
+ + + ); +} diff --git a/web/frontend/tailwind.config.ts b/web/frontend/tailwind.config.ts new file mode 100644 index 0000000..0b6e761 --- /dev/null +++ b/web/frontend/tailwind.config.ts @@ -0,0 +1,89 @@ +import type { Config } from "tailwindcss"; +import typography from "@tailwindcss/typography"; +import animate from "tailwindcss-animate"; + +const config: Config = { + darkMode: ["class"], + content: ["./index.html", "./src/**/*.{ts,tsx}"], + theme: { + container: { + center: true, + padding: "1rem", + screens: { + "2xl": "1200px", + }, + }, + extend: { + fontFamily: { + sans: ["Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "sans-serif"], + mono: ["JetBrains Mono", "SF Mono", "Monaco", "Cascadia Code", "monospace"], + }, + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + success: { + DEFAULT: "hsl(var(--success))", + foreground: "hsl(var(--success-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "fade-in": { + from: { opacity: "0", transform: "translateY(6px)" }, + to: { opacity: "1", transform: "translateY(0)" }, + }, + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "fade-in": "fade-in 0.3s ease-out", + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [typography, animate], +}; + +export default config; diff --git a/web/frontend/tsconfig.app.json b/web/frontend/tsconfig.app.json new file mode 100644 index 0000000..b6d27ad --- /dev/null +++ b/web/frontend/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/web/frontend/tsconfig.app.tsbuildinfo b/web/frontend/tsconfig.app.tsbuildinfo new file mode 100644 index 0000000..a9ab351 --- /dev/null +++ b/web/frontend/tsconfig.app.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/app.tsx","./src/main.tsx","./src/components/docuploader.tsx","./src/components/keywordtable.tsx","./src/components/linkform.tsx","./src/components/mdxrenderer.tsx","./src/components/navbar.tsx","./src/components/recentqueries.tsx","./src/components/ui/alert.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/lib/api.ts","./src/lib/mdx.tsx","./src/lib/utils.ts","./src/pages/doc.tsx","./src/pages/docslist.tsx","./src/pages/home.tsx","./src/pages/notfound.tsx","./src/pages/setup.tsx"],"version":"5.9.3"} \ No newline at end of file diff --git a/web/frontend/tsconfig.json b/web/frontend/tsconfig.json new file mode 100644 index 0000000..fec8c8e --- /dev/null +++ b/web/frontend/tsconfig.json @@ -0,0 +1,13 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/web/frontend/tsconfig.node.json b/web/frontend/tsconfig.node.json new file mode 100644 index 0000000..ecd65f5 --- /dev/null +++ b/web/frontend/tsconfig.node.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/web/frontend/tsconfig.node.tsbuildinfo b/web/frontend/tsconfig.node.tsbuildinfo new file mode 100644 index 0000000..62c7bf9 --- /dev/null +++ b/web/frontend/tsconfig.node.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./vite.config.ts"],"version":"5.9.3"} \ No newline at end of file diff --git a/web/frontend/vite.config.ts b/web/frontend/vite.config.ts new file mode 100644 index 0000000..4af253d --- /dev/null +++ b/web/frontend/vite.config.ts @@ -0,0 +1,25 @@ +import path from "node:path"; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// Dev server proxies API + redirect + legacy static to the Go backend on :8080. +// In production the Go binary serves the built assets directly via embed.FS. +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + server: { + port: 5173, + proxy: { + "/api": "http://localhost:8080", + "/query": "http://localhost:8080", + }, + }, + build: { + outDir: "dist", + emptyOutDir: true, + }, +}); diff --git a/web/static/styles.css b/web/static/styles.css deleted file mode 100644 index 84f6b35..0000000 --- a/web/static/styles.css +++ /dev/null @@ -1,376 +0,0 @@ -/* Dieter Rams-inspired GoLinks Theme */ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap'); - -:root { - /* Dieter Rams color palette - inspired by Braun design */ - --rams-white: #FFFFFF; - --rams-off-white: #FAFAFA; - --rams-light-grey: #E5E5E5; - --rams-medium-grey: #CCCCCC; - --rams-dark-grey: #666666; - --rams-charcoal: #333333; - --rams-black: #1A1A1A; - - /* Accent colors - minimal and functional */ - --rams-orange: #FF6B35; /* Braun orange */ - --rams-blue: #2563EB; /* Functional blue */ - --rams-green: #059669; /* Success green */ - --rams-red: #DC2626; /* Error red */ - --rams-yellow: #F59E0B; /* Warning yellow */ - - /* Typography */ - --font-primary: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - --font-mono: 'JetBrains Mono', 'SF Mono', Monaco, 'Cascadia Code', monospace; - - /* Spacing - based on 8px grid system */ - --space-xs: 0.25rem; /* 4px */ - --space-sm: 0.5rem; /* 8px */ - --space-md: 1rem; /* 16px */ - --space-lg: 1.5rem; /* 24px */ - --space-xl: 2rem; /* 32px */ - --space-2xl: 3rem; /* 48px */ - - /* Border radius - minimal, functional */ - --radius-sm: 2px; - --radius-md: 4px; - --radius-lg: 6px; - - /* Shadows - subtle and functional */ - --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); -} - -/* Reset and base styles */ -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html { - font-size: 16px; - line-height: 1.5; -} - -body { - font-family: var(--font-primary); - font-weight: 400; - color: var(--rams-charcoal); - background-color: var(--rams-off-white); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -/* Layout */ -.container { - max-width: 1200px; - margin: 0 auto; - padding: 0 var(--space-md); -} - -.constrained-width { - max-width: 800px; - margin: var(--space-md) auto; - padding: 0 var(--space-md); -} - -/* Typography */ -h1 { - font-family: var(--font-primary); - font-weight: 300; - font-size: 3rem; - letter-spacing: -0.02em; - color: var(--rams-white); - background: linear-gradient(135deg, var(--rams-black) 0%, var(--rams-charcoal) 100%); - text-align: center; - padding: var(--space-2xl) 0; - margin: 0 0 var(--space-2xl) 0; - position: relative; -} - -h1::after { - content: ''; - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); - width: 60px; - height: 2px; - background-color: var(--rams-orange); -} - -h1 .accent { - color: var(--rams-orange); - font-weight: 400; -} - -h2 { - font-family: var(--font-primary); - font-weight: 500; - font-size: 1.5rem; - color: var(--rams-charcoal); - margin: var(--space-2xl) 0 var(--space-lg) 0; - display: flex; - align-items: center; - gap: var(--space-sm); -} - -h2::before { - content: ''; - width: 4px; - height: 1.5rem; - background-color: var(--rams-orange); - border-radius: var(--radius-sm); -} - -h3 { - font-family: var(--font-primary); - font-weight: 500; - font-size: 1.25rem; - color: var(--rams-charcoal); - margin: var(--space-xl) 0 var(--space-md) 0; -} - -p { - margin-bottom: var(--space-md); - line-height: 1.6; - color: var(--rams-dark-grey); -} - -/* Code and monospace */ -code { - font-family: var(--font-mono); - font-size: 0.875rem; - font-weight: 500; - background-color: var(--rams-light-grey); - color: var(--rams-charcoal); - padding: var(--space-xs) var(--space-sm); - border-radius: var(--radius-sm); - border: 1px solid var(--rams-medium-grey); -} - -.url { - font-family: var(--font-mono); - font-size: 0.875rem; - word-break: break-all; -} - -/* Links */ -a { - color: var(--rams-blue); - text-decoration: none; - font-weight: 500; - transition: color 0.15s ease; -} - -a:hover { - color: var(--rams-orange); - text-decoration: underline; -} - -/* Forms - Dieter Rams functional design */ -#formData { - display: flex; - gap: 0; - margin: var(--space-lg) 0; - box-shadow: var(--shadow-md); - border-radius: var(--radius-lg); - overflow: hidden; - background: var(--rams-white); -} - -#formData input { - font-family: var(--font-primary); - font-size: 1rem; - padding: var(--space-md); - border: 1px solid var(--rams-medium-grey); - background: var(--rams-white); - color: var(--rams-charcoal); - transition: all 0.15s ease; - outline: none; -} - -#formData input:not(:last-child) { - border-right: none; -} - -#formData input:focus { - border-color: var(--rams-blue); - box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1); -} - -#formData input[type="text"] { - flex: 1; - min-width: 0; -} - -#formData input::placeholder { - color: var(--rams-dark-grey); - font-weight: 400; -} - -#formData input[type="submit"] { - background: linear-gradient(135deg, var(--rams-blue) 0%, var(--rams-orange) 100%); - color: var(--rams-white); - font-weight: 500; - border: none; - cursor: pointer; - padding: var(--space-md) var(--space-xl); - transition: all 0.15s ease; - position: relative; - overflow: hidden; -} - -#formData input[type="submit"]:hover { - transform: translateY(-1px); - box-shadow: var(--shadow-lg); -} - -#formData input[type="submit"]:active { - transform: translateY(0); -} - -/* Status messages - clean and functional */ -.status-message { - text-align: center; - padding: var(--space-md); - margin: calc(-1 * var(--space-2xl)) 0 var(--space-xl) 0; - border-radius: var(--radius-md); - font-weight: 500; - display: flex; - align-items: center; - justify-content: center; - gap: var(--space-sm); -} - -#success { - background-color: rgba(5, 150, 105, 0.1); - color: var(--rams-green); - border: 1px solid rgba(5, 150, 105, 0.2); -} - -#failure { - background-color: rgba(220, 38, 38, 0.1); - color: var(--rams-red); - border: 1px solid rgba(220, 38, 38, 0.2); -} - -/* Tables - minimal and functional */ -table { - width: 100%; - border-collapse: separate; - border-spacing: 0; - background: var(--rams-white); - border-radius: var(--radius-lg); - overflow: hidden; - box-shadow: var(--shadow-md); - margin: var(--space-lg) 0; -} - -table th { - background: linear-gradient(135deg, var(--rams-charcoal) 0%, var(--rams-black) 100%); - color: var(--rams-white); - font-family: var(--font-primary); - font-weight: 500; - font-size: 0.875rem; - text-transform: uppercase; - letter-spacing: 0.05em; - padding: var(--space-md); - text-align: left; - border-bottom: 2px solid var(--rams-orange); -} - -table td { - padding: var(--space-md); - border-bottom: 1px solid var(--rams-light-grey); - vertical-align: top; -} - -table tr:last-child td { - border-bottom: none; -} - -table tr:nth-child(even) { - background-color: rgba(229, 229, 229, 0.3); -} - -table tr:hover { - background-color: rgba(37, 99, 235, 0.05); -} - -/* Responsive design */ -@media (max-width: 768px) { - .constrained-width { - padding: 0 var(--space-sm); - } - - h1 { - font-size: 2rem; - padding: var(--space-xl) 0; - } - - #formData { - flex-direction: column; - border-radius: var(--radius-md); - } - - #formData input { - border-right: 1px solid var(--rams-medium-grey); - border-radius: 0; - } - - #formData input:first-child { - border-top-left-radius: var(--radius-md); - border-top-right-radius: var(--radius-md); - } - - #formData input:last-child { - border-bottom-left-radius: var(--radius-md); - border-bottom-right-radius: var(--radius-md); - } - - table { - font-size: 0.875rem; - } - - table th, - table td { - padding: var(--space-sm); - } -} - -/* Utility classes */ -.text-center { - text-align: center; -} - -.text-muted { - color: var(--rams-dark-grey); -} - -.mb-0 { margin-bottom: 0; } -.mb-1 { margin-bottom: var(--space-sm); } -.mb-2 { margin-bottom: var(--space-md); } -.mb-3 { margin-bottom: var(--space-lg); } - -.mt-0 { margin-top: 0; } -.mt-1 { margin-top: var(--space-sm); } -.mt-2 { margin-top: var(--space-md); } -.mt-3 { margin-top: var(--space-lg); } - -/* Loading states and animations */ -@keyframes fadeIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } -} - -.fade-in { - animation: fadeIn 0.3s ease-out; -} - -/* Focus management for accessibility */ -:focus-visible { - outline: 2px solid var(--rams-blue); - outline-offset: 2px; -} diff --git a/web/templates/homepage.html b/web/templates/homepage.html deleted file mode 100644 index 46a0a53..0000000 --- a/web/templates/homepage.html +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - golinks - - - - - -

golinks

- - {{if .Missing}} -
- ⚠️ -
Unable to find a shortcut for the query {{.Missing}}
-
- {{else if .Success}} -
- -
Added keyword {{.Success}}
-
- {{else if .Failure}} -
- -
Adding keyword {{.Failure}} failed: {{.Reason}}
-
- {{end}} - -
-

- If you're not yet setup with GoLinks then you should follow the guide - here. -

-

- Your search engine should now read: {{.BaseURL}}/query/%s -

- -

➕ Add new keyword

-
-
- - - -
-
- -
- - {{if .RecentQueries}} -

🔥 Popular queries

- - - - - - - - - - {{range .RecentQueries}} - - - - - - {{end}} - -
CountKeywordURL
{{.Count}}{{.Word}}{{urlify .Link}}
- {{end}} - - {{if .AllKeywords}} -

🔎 Full keyword list

-

- If you're needing inspiration, here are the current listed keywords. - Use {*} in a URL for variable links and space separated queries, - like go google cats. -

- - - - - - - - - - - {{range .AllKeywords}} - - - - - - - {{end}} - -
KeywordAliasesURLCreated On
{{.Word}}{{if .Aliases}}{{.Aliases}}{{else}}-{{end}}{{urlify .Link}}{{.CreatedAt.Format "2006-01-02"}}
- {{end}} -
- - - - diff --git a/web/templates/setup.html b/web/templates/setup.html deleted file mode 100644 index ef284fe..0000000 --- a/web/templates/setup.html +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - golinks - Setup - - - - -

golinks Setup

- -
-

🔧 Browser Setup

-

- To use GoLinks, you need to configure your browser to use our service as a search engine. - This allows you to type go keyword in your address bar and be redirected to the corresponding URL. -

- -

Chrome / Edge Setup

-
    -
  1. Open Chrome/Edge Settings
  2. -
  3. Go to Search engineManage search engines and site search
  4. -
  5. Click Add next to "Site search"
  6. -
  7. Fill in the form: -
      -
    • Search engine: GoLinks
    • -
    • Shortcut: go
    • -
    • URL with %s in place of query: {{.BaseURL}}/query/%s
    • -
    -
  8. -
  9. Click Add
  10. -
- -

Firefox Setup

-
    -
  1. Right-click in the address bar and select Add a Keyword for this Search
  2. -
  3. Or go to Bookmarks → Manage Bookmarks
  4. -
  5. Right-click and select New Bookmark
  6. -
  7. Fill in: -
      -
    • Name: GoLinks
    • -
    • Location: {{.BaseURL}}/query/%s
    • -
    • Keyword: go
    • -
    -
  8. -
  9. Click Save
  10. -
- -

Safari Setup

-
    -
  1. Safari doesn't support custom search engines directly
  2. -
  3. You can bookmark {{.BaseURL}}/homepage/ for easy access
  4. -
  5. Or use a browser extension like Keyword Search
  6. -
- -
- 💡 -
- Pro Tip: After setup, you can type go keyword in your address bar - to quickly navigate to any configured link! -
-
- -

🚀 Usage Examples

- - - - - - - - - - - - - - - - - - - - - - - - - -
CommandDescriptionExample
go docsNavigate to documentationIf "docs" points to https://docs.company.com
go jira 123Navigate with parametersIf "jira" contains {*} placeholder
go github myrepoSearch within a serviceDynamic search using {*}
- -

📝 Creating Links

-

- Once setup is complete, visit the homepage to: -

-
    -
  • Create new keyword shortcuts
  • -
  • View popular and recent queries
  • -
  • Browse all available keywords
  • -
  • Use {*} in URLs for dynamic content
  • -
- - -
- - - -