diff --git a/.agents/skills/golang-pro/SKILL.md b/.agents/skills/golang-pro/SKILL.md index 378bf7ea..4023f53e 100644 --- a/.agents/skills/golang-pro/SKILL.md +++ b/.agents/skills/golang-pro/SKILL.md @@ -97,9 +97,10 @@ Key properties demonstrated: bounded goroutine lifetime via `ctx`, error propaga - Handle all errors explicitly (no naked returns) - Write table-driven tests with subtests - Document all exported functions, types, and packages -- Use `X | Y` union constraints for generics (Go 1.18+) + - Use `X | Y` union constraints for generics (Go 1.18+) - Propagate errors with fmt.Errorf("%w", err) - Run race detector on tests (-race flag) +- Maintain pure domain entities (strict DDD): NEVER use `json:"..."`, `db:"..."` or other presentation/infrastructure tags in `internal/core/domain`. Map in **`internal/presentation/dto/`**: outbound **`{Type}FromDomain(domain.X)`** package funcs; inbound **`(r R) ToDomain()`** / **`ToPatch()`** on request DTOs. **Never pass `dto.*` to repositories** — only `domain.*` crosses the port boundary. ### MUST NOT DO - Ignore errors (avoid _ assignment without justification) diff --git a/.agents/skills/golang-pro/references/wpd-message-gateway.md b/.agents/skills/golang-pro/references/wpd-message-gateway.md index c3c3be72..64ebbcac 100644 --- a/.agents/skills/golang-pro/references/wpd-message-gateway.md +++ b/.agents/skills/golang-pro/references/wpd-message-gateway.md @@ -29,7 +29,7 @@ Router → Middleware → Handler → Service → Port ← Repository / Provider | Port | `internal/core/port/` | domain, `pkg/contracts` | implementations | | Service | `internal/core/service/` | port, domain, `pkg/contracts`, `pkg/registry` | SQL, HTTP, Echo | | Infrastructure | `internal/infrastructure/` | port, domain | presentation | -| Presentation | `internal/presentation/` | service, port, domain | direct SQL | +| Presentation | `internal/presentation/` | service, port, domain, **dto** | direct SQL; **inline HTTP DTOs in handlers**; **passing dto to port/repo** | | Public SDK | `pkg/*` | `pkg/*` only | `internal/*` | Sender interfaces (`EmailSender`, `SMSSender`, …) live in **`pkg/contracts/`**, not `internal/core/port/`. @@ -42,7 +42,7 @@ Sender interfaces (`EmailSender`, `SMSSender`, …) live in **`pkg/contracts/`** 2. Register via `init()` in `register.go` → `pkg/registry` 3. Blank-import in **`internal/app/imports.go`** (server mode only) -Do **not** add providers under `internal/infrastructure/provider/` (deprecated). +Add providers only under **`pkg/provider//`** (registered via `pkg/registry`, blank-imported in `internal/app/imports.go`). --- @@ -60,6 +60,7 @@ Do **not** add providers under `internal/infrastructure/provider/` (deprecated). - Wrap with `fmt.Errorf("…: %w", err)` - Domain/port sentinels: `port.ErrNotFound`, `port.ErrConflict`, etc. - Handlers: generic messages on 500; full error in slog only (`send_helper.go` pattern) +- **HTTP DTOs:** live in **`internal/presentation/dto/`** — never in handlers. Outbound: **`dto.XFromDomain(domain.Y)`** package funcs. Inbound: **`(r R) ToDomain()`** (→ `domain.*`) or **`ToPatch()`** (→ `service.*Patch`). **Ports/repos use `domain.*` only** — never import or accept `dto.*` (see `docs/backend/code-conventions.md`). ### Concurrency @@ -106,7 +107,7 @@ wpd-message-gateway/ │ ├── app/ # wire, imports, config │ ├── core/ # domain, port, service │ ├── infrastructure/ # postgres repos, logger, inbox store -│ └── presentation/ # router, handlers, middleware +│ └── presentation/ # router, handlers, middleware, dto/ ├── pkg/ │ ├── contracts/ # message types + sender interfaces │ ├── gateway/ # embedded SDK diff --git a/.agents/skills/software-architecture/SKILL.md b/.agents/skills/software-architecture/SKILL.md index 4aff70f7..8aa6af9f 100644 --- a/.agents/skills/software-architecture/SKILL.md +++ b/.agents/skills/software-architecture/SKILL.md @@ -40,6 +40,7 @@ This skill provides guidance for quality focused software development and archit - **Clean Architecture & DDD Principles:** - Follow domain-driven design and ubiquitous language - Separate domain entities from infrastructure concerns + - **Strict boundary**: Presentation data transfer objects (DTOs) must be completely separate from Domain entities. The Presentation layer is responsible for mapping Domain objects to DTOs. Domain objects must never contain presentation tags (e.g. JSON) and must never be serialized directly. - Keep business logic independent of frameworks - Define use cases clearly and keep them isolated - **Naming Conventions:** diff --git a/.agents/skills/software-architecture/references/wpd-message-gateway.md b/.agents/skills/software-architecture/references/wpd-message-gateway.md index 0acc68f0..b57e6c68 100644 --- a/.agents/skills/software-architecture/references/wpd-message-gateway.md +++ b/.agents/skills/software-architecture/references/wpd-message-gateway.md @@ -40,7 +40,7 @@ Router → Middleware → Handler → Service → Port ← Repository / Provider | `MGW.ARCH-PKG-INTERNAL` | `pkg/` imports `internal/` | | `MGW.ARCH-HANDLER-SQL` | SQL or provider SDK in handlers | | `MGW.ARCH-SERVICE-HTTP` | Echo/HTTP types in services | -| `MGW.ARCH-PROVIDER-INTERNAL` | New provider under `internal/infrastructure/provider/` | +| `MGW.ARCH-PROVIDER-INTERNAL` | New provider under `internal/` instead of `pkg/provider//` | | `MGW.ARCH-FAT-HANDLER` | Handler > ~200 lines or business logic in handler | **Prefer:** split handlers by portal concern (`portal_auth_handler`, `portal_workspace_handler`, …). Keep handlers thin — decode, call service, map response. diff --git a/.claude/commands/smell.md b/.claude/commands/smell.md index 16020700..dcd750e6 100644 --- a/.claude/commands/smell.md +++ b/.claude/commands/smell.md @@ -101,11 +101,11 @@ Router → Middleware → Handler → Service → Port ← Repository / Provider | Area | Violation | | ---- | --------- | -| `internal/core/domain/` | Imports Echo, HTTP, infra, or presentation | +| `internal/core/domain/` | Imports Echo, HTTP, infra, presentation, or contains struct tags (`json`, `db`) | | `internal/core/port/` | Contains implementation instead of interfaces | | `internal/core/service/` | Direct SQL/HTTP; missing `ctx`; orchestration left in handler | -| `internal/infrastructure/` | Business rules; bypasses port interfaces | -| `internal/presentation/` | Business logic; handler → repository without service; RBAC in handler instead of middleware | +| `internal/infrastructure/` | Business rules; bypasses port interfaces; imports `presentation/dto` | +| `internal/presentation/` | Business logic; handler → repository without service; RBAC in handler instead of middleware; **request/response structs or JSON mappers in `handler/*.go`** | | `pkg/gateway`, `pkg/contracts` | Imports `internal/*`; embedded SDK requires PostgreSQL | | Providers | No `init()` registration or missing blank import in `internal/app/imports.go` | | Auth | Portal mutations without JWT + `RequirePermission`; internal ingest open outside local dev | @@ -184,6 +184,8 @@ Do not invent issues. If the diff is clean, say so. | MGW.MIGRATION | Migration hygiene failure | | MGW.LOG-SECRET | Secrets or PII in logs | | MGW.N+1-QUERY | Query inside a loop | +| MGW.HANDLER-DTO | HTTP struct or mapper (`XFromDomain` / `ToDomain` / `ToPatch`) in `handler/*.go` instead of `presentation/dto/` | +| MGW.DTO-IN-REPO | Repository or port interface accepts `presentation/dto.*` instead of `domain.*` | | MGW.GOROUTINE-LEAK | Goroutine without lifecycle control | **Frontend** @@ -219,6 +221,7 @@ Do not invent issues. If the diff is clean, say so. | -- | ------- | | DDD.LAYER | Business rule or orchestration in the wrong layer (handler/UI/infra) | | DDD.BOUNDARY | Bounded context violated (feature↔feature, `pkg`→`internal`, shared→feature) | +| DDD.DOMAIN-LEAK | Domain object returned directly from handler without DTO mapping, or containing presentation tags (`json`) | | DDD.VALUE-TYPE | Domain concept as raw string/number — use typed constant in domain + API/FE mirror | **General — SOLID** diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1247e62..d0b33a42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -201,13 +201,19 @@ jobs: # Wait for server - name: Wait for server to be ready run: | + ready=0 for i in {1..30}; do - if curl -s http://localhost:10101/health > /dev/null 2>&1; then + if curl -sf http://localhost:10101/health > /dev/null 2>&1; then echo "Server ready" + ready=1 break fi sleep 1 done + if [ "$ready" -ne 1 ]; then + echo "Server did not become ready within 30s" + exit 1 + fi - name: Install Bruno CLI run: npm install -g @usebruno/cli diff --git a/Makefile b/Makefile index bdbc0c02..5822ca87 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: it install upgrade setup specify spec clarify clr plan pln tasks tsk implement impl analyze alyz checklist chk pr sync agents agents-kill start stop test audit build clean docker-check dev dev-down seed-demo help ui-install ui ui-build ui-format ui-test ui-lint storybook +.PHONY: it install upgrade setup specify spec clarify clr plan pln tasks tsk implement impl analyze alyz checklist chk pr sync agents agents-kill start stop test audit build clean docker-check dev dev-down db-init seed-demo help ui-install ui ui-build ui-format ui-test ui-lint storybook # ============================================================================ # ANSI Color Codes @@ -300,11 +300,19 @@ docker-check: dev: docker-check @printf "\n" @printf "$(BOLD)$(CYAN)🐳 Starting Gateway, database, and Portal UI via Docker...$(RESET)\n" - @docker compose up -d --build + @docker compose up -d --build --renew-anon-volumes @printf "$(GREEN)✅ All services started!$(RESET)\n" @printf "\n" - @printf " $(BOLD)Gateway API:$(RESET) http://localhost:10101\n" - @printf " $(BOLD)Portal UI:$(RESET) http://localhost:10104\n" + @printf " ╭───────────────────────────────────────────────────╮\n" + @printf " │ │\n" + @printf " │ $(BOLD)Gateway API:$(RESET) http://localhost:10101 │\n" + @printf " │ $(BOLD)Portal UI:$(RESET) http://localhost:10104 │\n" + @printf " │ │\n" + @printf " │ $(BOLD)Demo Account$(RESET) │\n" + @printf " │ Email: demo@weprodev.com │\n" + @printf " │ Password: secret │\n" + @printf " │ │\n" + @printf " ╰───────────────────────────────────────────────────╯\n" @printf "\n" ## Stop Docker Compose @@ -314,13 +322,22 @@ dev-down: docker-check @docker compose down @printf "$(GREEN)✅ Stopped!$(RESET)\n" -## Re-apply SQL seeds to the running Postgres container (demo user, RBAC, providers) -seed-demo: docker-check +## Apply schema when Postgres volume exists but tables are missing (then restart gateway) +db-init: docker-check @printf "\n" - @printf "$(BOLD)$(CYAN)🌱 Applying database seeds...$(RESET)\n" + @printf "$(BOLD)$(CYAN)🗄️ Initializing database schema...$(RESET)\n" @docker compose exec -T -e POSTGRES_USER=gateway -e POSTGRES_DB=gateway db \ - bash /docker-entrypoint-initdb.d/apply-seeds.sh /docker-entrypoint-initdb.d/seeds - @printf "$(GREEN)✅ Seeds applied (demo@weprodev.com / secret)$(RESET)\n" + bash /docker-entrypoint-initdb.d/init-db.sh + @docker compose restart gateway + @printf "$(GREEN)✅ Database ready — gateway will apply seeds on startup (demo@weprodev.com / secret)$(RESET)\n" + @printf "\n" + +## Re-apply SQL seeds by restarting the gateway (seeds run on every startup) +seed-demo: docker-check + @printf "\n" + @printf "$(BOLD)$(CYAN)🌱 Re-applying database seeds (gateway restart)...$(RESET)\n" + @docker compose restart gateway + @printf "$(GREEN)✅ Seeds applied on gateway startup (demo@weprodev.com / secret)$(RESET)\n" @printf "\n" @@ -368,7 +385,8 @@ help: @printf "$(BOLD)$(GREEN)🐳 Docker$(RESET)\n" @printf " $(YELLOW)make dev$(RESET) Start Gateway, DB, and UI via Docker (with hot-reloading)\n" @printf " $(YELLOW)make dev-down$(RESET) Stop Docker containers\n" - @printf " $(YELLOW)make seed-demo$(RESET) Re-apply SQL seeds (demo@weprodev.com / secret)\n" + @printf " $(YELLOW)make db-init$(RESET) Apply schema if DB volume is empty; gateway seeds on start\n" + @printf " $(YELLOW)make seed-demo$(RESET) Re-apply seeds via gateway restart\n" @printf "\n" @printf "$(BOLD)$(CYAN)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$(RESET)\n" diff --git a/Readme.md b/Readme.md index 7db6392b..84c00018 100644 --- a/Readme.md +++ b/Readme.md @@ -108,12 +108,12 @@ Seeded on first DB init. Reset anytime: `make seed-demo`. |--|--| | Portal | `demo@weprodev.com` / `secret` | | API key | `demo-client-id` / `demo-secret` | -| Workspace key | `demo` | +| Workspace ID | `00000000-0000-0000-0000-000000000001` | ```bash curl -X POST http://localhost:10101/v1/email \ -u "demo-client-id:demo-secret" \ - -H "X-Workspace-Key: demo" \ + -H "X-Workspace-Key: 00000000-0000-0000-0000-000000000001" \ -H "Content-Type: application/json" \ -d '{"to":["user@example.com"],"subject":"Hello","html":"

World

"}' ``` diff --git a/cmd/server/init.go b/cmd/server/init.go new file mode 100644 index 00000000..df218735 --- /dev/null +++ b/cmd/server/init.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + + pkglogger "github.com/weprodev/go-pkg/logger" + + "github.com/weprodev/wpd-message-gateway/internal/app" + applogger "github.com/weprodev/wpd-message-gateway/internal/infrastructure/logger" +) + +func loadAppConfig(configPath string) (*app.Config, error) { + return app.LoadAppConfig(configPath) +} + +func validateAppConfig(cfg *app.Config) error { + if err := app.ValidateConfig(cfg); err != nil { + return fmt.Errorf("configuration error: %w (hint: copy configs/local.example.yml to configs/local.yml)", err) + } + return nil +} + +func setLogger(cfg *app.Config) (*pkglogger.Logger, error) { + return applogger.New(cfg.EnvironmentName()) +} + +func wireApplication(cfg *app.Config) (*app.Application, error) { + sysLogger, err := setLogger(cfg) + if err != nil { + return nil, fmt.Errorf("init logger: %w", err) + } + return app.Wire(cfg, sysLogger) +} diff --git a/cmd/server/main.go b/cmd/server/main.go index 8d76101b..267c02b6 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,74 +1,69 @@ package main import ( + "context" + "flag" "fmt" "log/slog" + "net/http" "os" - - "github.com/weprodev/wpd-message-gateway/internal/app" - "github.com/weprodev/wpd-message-gateway/internal/infrastructure/logger" + "os/signal" + "syscall" + "time" ) func main() { - // Initialise the structured logger first so every subsequent slog call is - // formatted and levelled correctly. Uses APP_ENVIRONMENT to pick text (local) - // vs JSON (production) format. - env := os.Getenv("APP_ENVIRONMENT") - sysLogger, err := logger.New(env) - if err != nil { - // Logger not yet up — fall back to bare stderr before exiting. - fmt.Fprintf(os.Stderr, "FATAL: init logger: %v\n", err) + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "fatal error: %v\n", err) os.Exit(1) } +} + +func run() error { + configPath := flag.String("config", "", "Path to config file (default: CONFIG_PATH env or configs/local.yml)") + flag.Parse() - configPath := os.Getenv("CONFIG_PATH") - cfg, err := app.LoadConfig(configPath) + cfg, err := loadAppConfig(*configPath) if err != nil { - slog.Error("failed to load config", "error", err) - os.Exit(1) + return fmt.Errorf("load config: %w", err) } - if err := app.ValidateConfig(cfg); err != nil { - slog.Error("configuration error", - "error", err, - "hint", "each message type requires a default provider (e.g. memory, mailgun); "+ - "copy configs/local.example.yml to configs/local.yml and configure providers", - ) - os.Exit(1) + if err := validateAppConfig(cfg); err != nil { + return err } - application, err := app.Wire(cfg, sysLogger) + application, err := wireApplication(cfg) if err != nil { - slog.Error("failed to initialise application", "error", err) - os.Exit(1) + return fmt.Errorf("wire application: %w", err) } - logConfiguration(cfg) + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() - port := resolvePort(cfg) - slog.Info("gateway server starting", "port", port, "environment", cfg.Environment) + cfg.LogSummary() + slog.Info("gateway server starting") - if err := application.Echo.Start(":" + port); err != nil { - slog.Error("server stopped", "error", err) - os.Exit(1) + errCh := make(chan error, 1) + go func() { + if err := application.Echo.Start(cfg.ListenAddr()); err != nil && err != http.ErrServerClosed { + errCh <- err + } + }() + + select { + case err := <-errCh: + return fmt.Errorf("server stopped unexpectedly: %w", err) + case <-ctx.Done(): + slog.Info("shutdown signal received") } -} -func logConfiguration(cfg *app.Config) { - slog.Info("loaded configuration", - "email_provider", cfg.DefaultEmailProvider(), - "sms_provider", cfg.DefaultSMSProvider(), - "push_provider", cfg.DefaultPushProvider(), - "chat_provider", cfg.DefaultChatProvider(), - ) -} + shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() -func resolvePort(cfg *app.Config) string { - if port := os.Getenv("PORT"); port != "" { - return port - } - if cfg.Server.Port != 0 { - return fmt.Sprintf("%d", cfg.Server.Port) + if err := application.Shutdown(shutdownCtx); err != nil { + return fmt.Errorf("graceful shutdown: %w", err) } - return "10101" + + slog.Info("server stopped") + return nil } diff --git a/database/apply-seeds.sh b/database/apply-seeds.sh deleted file mode 100755 index 34cceb3a..00000000 --- a/database/apply-seeds.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -# Apply idempotent SQL seeds in order. Used by init-db.sh and `make seed-demo`. -set -euo pipefail - -SEEDS_DIR="${1:-/docker-entrypoint-initdb.d/seeds}" -PSQL=(psql -v ON_ERROR_STOP=1 --username "${POSTGRES_USER:?}" --dbname "${POSTGRES_DB:?}") - -for seed in \ - 001_seed_permissions.sql \ - 002_seed_providers.sql \ - 003_seed_mailgun_config.sql \ - 004_demo_workspace.sql -do - echo "→ seeds/$seed" - "${PSQL[@]}" -f "$SEEDS_DIR/$seed" -done diff --git a/database/init-db.sh b/database/init-db.sh index 00958835..f13789a7 100755 --- a/database/init-db.sh +++ b/database/init-db.sh @@ -3,12 +3,16 @@ set -e echo "=== Running Database Init Script ===" +TABLE_EXISTS=$(psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -tAc \ + "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'users');") + +if [ "$TABLE_EXISTS" = "t" ]; then + echo "Schema already present — skipping migration." + exit 0 +fi + echo "Applying migrations..." psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \ -f /docker-entrypoint-initdb.d/migrations/20260318000000_init_gateway.up.sql -echo "Applying seeds..." -export POSTGRES_USER POSTGRES_DB -bash /docker-entrypoint-initdb.d/apply-seeds.sh /docker-entrypoint-initdb.d/seeds - -echo "=== Database Init Script Completed ===" +echo "=== Database Init Script Completed (seeds run on gateway startup) ===" diff --git a/database/migrations/20260318000000_init_gateway.down.sql b/database/migrations/20260318000000_init_gateway.down.sql index 8dec95e1..1601b1c7 100644 --- a/database/migrations/20260318000000_init_gateway.down.sql +++ b/database/migrations/20260318000000_init_gateway.down.sql @@ -6,6 +6,7 @@ DROP TABLE IF EXISTS workspace_access_audits CASCADE; DROP TABLE IF EXISTS workspace_settings CASCADE; DROP TABLE IF EXISTS template_components CASCADE; DROP TABLE IF EXISTS templates CASCADE; +DROP TABLE IF EXISTS message_request_payloads CASCADE; DROP TABLE IF EXISTS message_request_logs CASCADE; DROP TABLE IF EXISTS api_keys CASCADE; DROP TABLE IF EXISTS integrations CASCADE; diff --git a/database/migrations/20260318000000_init_gateway.up.sql b/database/migrations/20260318000000_init_gateway.up.sql index 5b9bc09f..07207d64 100644 --- a/database/migrations/20260318000000_init_gateway.up.sql +++ b/database/migrations/20260318000000_init_gateway.up.sql @@ -62,6 +62,9 @@ CREATE TRIGGER trg_workspaces_set_updated_at FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE INDEX idx_workspaces_owner_id ON workspaces(owner_id); +CREATE INDEX idx_workspaces_active_list + ON workspaces (status, is_private, name) + WHERE status = 'active'; -- ============================================================================== -- 3. ROLES TABLE (RBAC) @@ -70,7 +73,7 @@ CREATE INDEX idx_workspaces_owner_id ON workspaces(owner_id); CREATE TABLE roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, - guard_name TEXT NOT NULL DEFAULT 'web', + guard_name TEXT NOT NULL DEFAULT 'msg_web', display_name TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), @@ -123,7 +126,9 @@ CREATE UNIQUE INDEX idx_invitations_pending_per_workspace_email CREATE INDEX idx_invitations_workspace_id ON invitations(workspace_id); CREATE INDEX idx_invitations_token_hash ON invitations(token_hash); -CREATE INDEX idx_invitations_role_id ON invitations(role_id); +CREATE INDEX idx_invitations_workspace_pending + ON invitations (workspace_id, status, expires_at) + WHERE status = 'pending'; -- ============================================================================== -- 6. WORKSPACE_CHANNELS TABLE @@ -186,6 +191,8 @@ CREATE TRIGGER trg_provider_config_fields_set_updated_at BEFORE UPDATE ON provider_config_fields FOR EACH ROW EXECUTE FUNCTION set_updated_at(); +CREATE INDEX idx_provider_config_fields_provider_id ON provider_config_fields(provider_id); + -- ============================================================================== -- 9. INTEGRATIONS TABLE (Workspace ↔ Provider Credentials) -- ============================================================================== @@ -217,6 +224,8 @@ CREATE UNIQUE INDEX idx_integrations_default_per_channel CREATE INDEX idx_integrations_provider_id ON integrations(provider_id); CREATE INDEX idx_integrations_workspace_channel ON integrations(workspace_id, channel_type); +CREATE INDEX idx_integrations_workspace_channel_status + ON integrations (workspace_id, channel_type, status, is_default DESC, created_at DESC); CREATE TRIGGER trg_integrations_set_updated_at BEFORE UPDATE ON integrations @@ -241,6 +250,7 @@ CREATE TABLE api_keys ( ); CREATE INDEX idx_api_keys_workspace_id ON api_keys(workspace_id); +CREATE INDEX idx_api_keys_workspace_created ON api_keys (workspace_id, created_at DESC); -- ============================================================================== -- 11. MESSAGE_REQUEST_LOGS TABLE (Append-only) @@ -258,6 +268,7 @@ CREATE TABLE message_request_logs ( request_id VARCHAR(64), duration_ms INT CHECK (duration_ms >= 0), error_message TEXT, + inbox_message_id UUID, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); @@ -272,6 +283,24 @@ CREATE INDEX idx_message_request_logs_workspace_api_created ON message_request_logs (workspace_id, api_key_id, created_at DESC) WHERE api_key_id IS NOT NULL; +CREATE INDEX idx_message_request_logs_workspace_channel_created + ON message_request_logs (workspace_id, channel_type, created_at DESC); + +CREATE INDEX idx_message_request_logs_workspace_inbox_message + ON message_request_logs (workspace_id, inbox_message_id) + WHERE inbox_message_id IS NOT NULL; + +-- ============================================================================== +-- 11.5. MESSAGE_REQUEST_PAYLOADS TABLE (Append-only body storage) +-- ============================================================================== + +CREATE TABLE message_request_payloads ( + log_id UUID PRIMARY KEY REFERENCES message_request_logs(id) ON DELETE CASCADE, + request_body TEXT, + response_body TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + -- ============================================================================== -- 12. TEMPLATES TABLE -- ============================================================================== @@ -370,7 +399,7 @@ CREATE INDEX idx_workspace_access_audits_actor_user_id CREATE TABLE permissions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, - guard_name TEXT NOT NULL DEFAULT 'web', + guard_name TEXT NOT NULL DEFAULT 'msg_web', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), diff --git a/database/seeds/001_seed_permissions.sql b/database/seeds/001_seed_permissions.sql index 5cc17afb..d13a1eb7 100644 --- a/database/seeds/001_seed_permissions.sql +++ b/database/seeds/001_seed_permissions.sql @@ -1,31 +1,31 @@ -- Default RBAC Permissions & Role Bindings for WPD Message Gateway -- 1. Insert default roles -INSERT INTO roles (id, name, display_name) +INSERT INTO roles (id, name, display_name, guard_name) VALUES - ('00000000-0000-0000-0000-000000000020', 'admin', 'Admin'), - ('00000000-0000-0000-0000-000000000021', 'member', 'Member') + ('00000000-0000-0000-0000-000000000020', 'admin', 'Admin', 'msg_web'), + ('00000000-0000-0000-0000-000000000021', 'member', 'Member', 'msg_web') ON CONFLICT (name, guard_name) DO NOTHING; -- 2. Insert permissions INSERT INTO permissions (id, name, guard_name) VALUES - (gen_random_uuid(), 'workspaces.read', 'web'), - (gen_random_uuid(), 'workspaces.write', 'web'), - (gen_random_uuid(), 'members.read', 'web'), - (gen_random_uuid(), 'members.write', 'web'), - (gen_random_uuid(), 'apikeys.read', 'web'), - (gen_random_uuid(), 'apikeys.write', 'web'), - (gen_random_uuid(), 'logs.read', 'web'), - (gen_random_uuid(), 'integrations.read', 'web'), - (gen_random_uuid(), 'integrations.write', 'web'), - (gen_random_uuid(), 'templates.read', 'web'), - (gen_random_uuid(), 'templates.write', 'web'), - (gen_random_uuid(), 'settings.read', 'web'), - (gen_random_uuid(), 'settings.write', 'web'), - (gen_random_uuid(), 'invitations.read', 'web'), - (gen_random_uuid(), 'invitations.write', 'web'), - (gen_random_uuid(), 'send.test', 'web') + (gen_random_uuid(), 'workspaces.read', 'msg_web'), + (gen_random_uuid(), 'workspaces.write', 'msg_web'), + (gen_random_uuid(), 'members.read', 'msg_web'), + (gen_random_uuid(), 'members.write', 'msg_web'), + (gen_random_uuid(), 'apikeys.read', 'msg_web'), + (gen_random_uuid(), 'apikeys.write', 'msg_web'), + (gen_random_uuid(), 'logs.read', 'msg_web'), + (gen_random_uuid(), 'integrations.read', 'msg_web'), + (gen_random_uuid(), 'integrations.write', 'msg_web'), + (gen_random_uuid(), 'templates.read', 'msg_web'), + (gen_random_uuid(), 'templates.write', 'msg_web'), + (gen_random_uuid(), 'settings.read', 'msg_web'), + (gen_random_uuid(), 'settings.write', 'msg_web'), + (gen_random_uuid(), 'invitations.read', 'msg_web'), + (gen_random_uuid(), 'invitations.write', 'msg_web'), + (gen_random_uuid(), 'inbox.write', 'msg_web') ON CONFLICT (name, guard_name) DO NOTHING; -- 3. Bind permissions to 'admin' role @@ -36,7 +36,7 @@ CROSS JOIN roles r WHERE r.name = 'admin' ON CONFLICT (permission_id, role_id) DO NOTHING; --- 4. Bind permissions to 'member' role (read-only + send.test) +-- 4. Bind permissions to 'member' role (read-only + inbox.write) INSERT INTO role_has_permissions (permission_id, role_id) SELECT p.id, r.id FROM permissions p @@ -51,6 +51,6 @@ WHERE r.name = 'member' 'templates.read', 'settings.read', 'invitations.read', - 'send.test' + 'inbox.write' ) ON CONFLICT (permission_id, role_id) DO NOTHING; diff --git a/docs/agents/delivery-agent.md b/docs/agents/delivery-agent.md index 622908a4..aed90fd2 100644 --- a/docs/agents/delivery-agent.md +++ b/docs/agents/delivery-agent.md @@ -16,7 +16,7 @@ Orchestration note: the **[master-agent](./master-agent.md)** classifies request - **Traceability:** Before multi-file edits, state a **short plan** (files/areas + verification). After edits, summarize **what changed** and **how it was verified**. - **Bounded retries:** If [verification](./verification.md) fails twice for the **same** mistake class, **stop**, re-read output, and **narrow scope** instead of looping. - **No secret material (Data Exposure limits):** Do not echo or commit API keys, JWTs, or pasted credentials. **Hard requirement:** Any code generating secrets must use crypto-safe `crypto/rand`, and secrets must exclusively live in ENV or hashed in Postgres. -- **Engineering principles (implement time):** Apply **KISS** (smallest correct diff), **DRY** (extract on second duplication), **DDD** (domain constants/types in `internal/core/domain/` + FE `*.types.ts`), **SOLID** (thin handlers, services orchestrate, ports for I/O). Remove dead code and stale comments/docs you touch — do not leave refactor debris. +- **Engineering principles (implement time):** Apply **KISS**, **DRY**, **DDD**, **SOLID**. Portal HTTP shapes in **`internal/presentation/dto/`**; map to **`domain.*`** before calling services — **never pass DTOs to repositories**. - **Comments:** Add only when behavior is non-obvious; delete comments that restate code or became wrong after your edit. ## 1. Safety & Robustness Pre-Check diff --git a/docs/agents/review-agent.md b/docs/agents/review-agent.md index 299c8044..3215afd8 100644 --- a/docs/agents/review-agent.md +++ b/docs/agents/review-agent.md @@ -68,6 +68,7 @@ Derived from [backend-engineer.md](../backend/backend-engineer.md) and [code-con - [ ] **Database Schema**: If columns/tables changed, the DB diagram and relative docs must be updated. - [ ] **Robustness & Scalability**: No goroutine leaks. - [ ] **Tests**: Table-driven tests covering **edge cases** and **expected failures**. +- [ ] **Presentation DTOs**: Types/mappers in `internal/presentation/dto/`; **`domain.*` only** on port/repo interfaces (`MGW.HANDLER-DTO`, `MGW.DTO-IN-REPO`). - [ ] **Principles**: Domain constants for statuses/enums; no handler business rules; no `pkg`→`internal` (`DDD.VALUE-TYPE`, `DDD.LAYER`, `SOLID.DIP`). ## 4. Frontend review checklist @@ -87,7 +88,7 @@ Derived from [frontend-engineer.md](../frontend/frontend-engineer.md) and [conve - [ ] **Architecture Check**: If a new layer or directory was created, does it align with the documented architecture? Are docs updated? - [ ] **Anti-patterns**: Ensure new patterns introduced do not conflict with existing boundaries. -- [ ] **No stale docs**: Portal capability claims match code (remove “REST only” when UI exists). +- [ ] **No stale docs**: Portal capability claims match code (UI pages, permissions, and API tables stay in sync with `frontend/src/core/router/` and `internal/presentation/router.go`). ## 6. Severity guidance & Reporting diff --git a/docs/backend/architecture.md b/docs/backend/architecture.md index c473262b..674c7670 100644 --- a/docs/backend/architecture.md +++ b/docs/backend/architecture.md @@ -138,7 +138,7 @@ gw.SendEmail(ctx, &contracts.Email{ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ GatewayService │ │ │ │ SendEmail() · SendSMS() · SendPush() · SendChat() │ │ -│ │ Reads dispatch_mode from workspace_settings │ │ +│ │ Reads message_dispatch_mode from workspace_settings │ │ │ │ Reads provider credentials from integrations table │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ┌──────────────────────────────────────────────────────────┐ │ @@ -186,7 +186,7 @@ Portal accounts use **email + password** (passwords are stored hashed). All mana The core service layer decouples from `wpd-gogate` by depending on the `port.AuthorizationGate` interface. The implementation adapter lives in `internal/infrastructure/authgate/gate_adapter.go`. - **admin**: Full read/write access to settings, integrations, templates, API keys, and member removal. Assigned automatically to the creator of a workspace. -- **member**: Read-only access to workspaces, settings, templates, API keys, plus permission to send test messages (`send.test`). +- **member**: Read-only access to workspaces, settings, templates, API keys, and logs, plus inbox mutations (`inbox.write`). Public workspaces (`is_private = false`) are dynamically accessible to any authenticated user as a `"viewer"` with read-only permissions (i.e. `*.read` operations are bypassed and approved automatically without requiring explicit membership or Casbin checks). All write operations on public workspaces remain strictly restricted to workspace admins. @@ -200,7 +200,7 @@ Authorization: Bearer OR X-Api-Client-Id: wk_abc123 X-Api-Client-Secret: -X-Workspace-Key: +X-Workspace-Key: ``` ### Dual Authorization Design @@ -231,27 +231,46 @@ Provider credentials are stored AES-encrypted in the `integrations` table. Confi ### Portal UI coverage -The React Portal (`frontend/`) currently implements: auth, workspace list, **integrations**, message **logs**, and **send test**. API keys, templates, members, settings, and memory inbox browsing are REST/Bruno only. +The React Portal (`frontend/`) includes: + +| Area | Pages | +| ---- | ----- | +| **Auth** | Register, sign in | +| **Workspaces** | List, create, join | +| **Logs** | Overview (all channels) + per-channel tabs (SMS, push, chat) | +| **Email** | Captured inbox browser, templates, Get started curl guide | +| **Integrations** | Connect and manage providers | +| **Settings** | General, API keys, message dispatch | + +**API-only today:** team invites/members UI (placeholder), SMS/push/chat captured-message browsers (email inbox only), internal inbox ingest. --- -## Message Dispatch Modes +## Message Dispatch & Content Storage + +Each workspace controls outbound routing and inbox content capture with **two orthogonal settings**: -Each workspace independently controls how outbound messages are handled: +| Setting | Values | Purpose | +| ------- | ------ | ------- | +| `message_dispatch_mode` | `memory` (default) \| `provider` | Where the message is routed | +| `store_message_content` | `false` (default) \| `true` | Whether message bodies are captured in the portal inbox (in-process) | -| Mode | Behavior | -| --------------------- | ----------------------------------------------------------------------------------- | -| `memory_only` | Captured in-process RAM only. No external provider called. Default for development. | -| `provider_only` | Sent through the connected integration only. No in-memory copy. | -| `memory_and_provider` | Stored in memory AND sent through the integration. | +Portal **Settings → Message Dispatch** exposes two controls that map directly to these keys: -Configured via `PATCH /api/v1/workspaces/:wid/settings` (REST — no Portal UI page yet): +| UI control | Setting key | Values | +| ---------- | ----------- | ------ | +| Dispatch mode (radio) | `message_dispatch_mode` | `memory` \| `provider` | +| Store message content (checkbox) | `store_message_content` | `false` (off) \| `true` (on) | + +Configured via `PATCH /api/v1/workspaces/:wid/settings` (Portal **Settings → Message Dispatch**): ``` PATCH /api/v1/workspaces/:wid/settings -{ "message_dispatch_mode": "provider_only" } +{ "message_dispatch_mode": "provider", "store_message_content": "true" } ``` +**Request logs**: Every API send appends a row to `message_request_logs` (operational tracing with correlation `request_id`). Payload bodies are stored in a separate table (`message_request_payloads`) to prevent table bloat and enforce independent data privacy/retention policies. + --- ## Request Flow: Sending a Message @@ -263,31 +282,31 @@ HTTP Client (your app) │ │ POST /v1/email │ Authorization: Bearer - │ X-Workspace-Key: myapp + │ X-Workspace-Key: │ { "to": [...], "subject": "...", "html": "..." } ▼ APIKeyAuthMiddleware │ Validates workspace-jwt OR client_id+secret - │ Resolves workspace UUID from X-Workspace-Key + │ Resolves workspace from X-Workspace-Key (UUID or slug) │ Sets workspace_id in request context ▼ GatewayHandler.HandleSendEmail() │ Reads workspace_id from context ▼ GatewayService.SendEmail(ctx, workspaceID, email) - │ Reads workspace_settings.message_dispatch_mode from DB + │ Reads message_dispatch_mode + store_message_content from DB │ - ├── memory_only → Memory Provider (in-process RAM) - │ │ - │ ▼ - │ Portal Inbox (SSE + REST) + ├── memory → Memory Provider (in-process RAM) + │ │ + │ ▼ + │ Portal Inbox (SSE + REST) │ - ├── provider_only → reads integrations table for workspace+channel - │ Decrypts AES config - │ Instantiates provider via registry - │ → sends to Mailgun/etc + ├── provider → reads integrations table for workspace+channel + │ Decrypts AES config + │ Instantiates provider via registry + │ → sends to Mailgun/etc │ - └── memory_and_provider → both paths above + └── store_message_content=true → also persists content via inbox writer ``` ### SDK Mode — `gateway.New(config).SendEmail(...)` @@ -322,7 +341,7 @@ Workspace "myapp" ├── Integrations (provider credentials per channel) │ ├── email: mailgun { api_key: "...", domain: "..." } │ └── sms: (not configured) -├── Settings { message_dispatch_mode: "provider_only" } +├── Settings { message_dispatch_mode: "provider", store_message_content: "false" } ├── Templates (email HTML templates) └── Message Logs (request audit trail) ``` @@ -438,19 +457,19 @@ Only create new files in pkg/provider// ### 5. Memory Provider & Dispatch Modes -The **memory provider** captures messages in process RAM. It's always available — no external service needed. Combined with `dispatch_mode`, you can: +The **memory provider** captures messages in process RAM. It's always available — no external service needed. Combined with workspace settings, you can: -- Use `memory_only` for local development (captured messages via inbox REST API / Bruno) -- Use `provider_only` in production (messages go to real provider) -- Use `memory_and_provider` to keep a local copy AND send to the real provider +- Use `message_dispatch_mode=memory` for local development (captured messages via inbox REST API / Bruno) +- Use `message_dispatch_mode=provider` in production (messages go to the real provider) +- Set `store_message_content=true` to persist message bodies regardless of routing mode ### 6. Portal (always on) The React Portal runs at `portal.ui_port` (default **10104**) when the server starts. -**Portal UI today:** sign in, list workspaces, manage **integrations**, view message logs, send test messages. +**Portal UI:** sign in, manage workspaces, view request logs, browse the **email** captured inbox, manage **email templates**, configure **integrations** and **settings** (general, API keys, message dispatch). -**REST only (no UI pages yet):** workspace create, API keys, templates, members, settings, memory inbox browser. +**API-only:** team member management, SMS/push/chat captured-message browsers, internal inbox ingest. --- @@ -459,9 +478,9 @@ The React Portal runs at `portal.ui_port` (default **10104**) when the server st To enable robust trace tracking across all layers, the gateway employs an end-to-end correlation ID pipeline: 1. **Correlation Generation**: Echo's `RequestID` middleware generates a unique request UUID for every incoming HTTP request, placing it in the `X-Request-ID` header. -2. **Context Propagation**: A custom correlation middleware extracts this request ID and injects it into the standard Go `context.Context` (accessible via `logger.GetRequestID(ctx)`). -3. **Structured slog Hooking**: An application-wide custom `ContextHandler` intercepts all standard `slog.InfoContext` / `slog.ErrorContext` calls. It transparently extracts the `request_id`, `workspace_id`, `api_key_id`, `channel`, and `provider` attributes from the context and automatically appends them to printed log records without requiring manual logging boilerplate. -4. **Audit Trail Tracking**: The presentation-layer `SendHelper` automatically populates the `request_id` column on the `message_request_logs` PostgreSQL table, linking individual requests directly to database audit trails. +2. **Context Propagation**: Router middleware copies the request ID into `context.Context` via `logger.WithRequestID`. API key auth also calls `logger.WithWorkspace` so `workspace_id` and `api_key_id` appear on all `/v1/*` logs. Send handlers add `channel` and `provider`. +3. **Structured slog**: go-pkg `ContextExtractors` append correlation fields to every `slog.*Context` record (`request_id`, `workspace_id`, `api_key_id`, `channel`, `provider`). +4. **Audit Trail**: `SendHelper` persists `request_id` on every row in `message_request_logs`, linking HTTP requests to the operational audit trail. --- diff --git a/docs/backend/code-conventions.md b/docs/backend/code-conventions.md index f11b9796..e247f6bc 100644 --- a/docs/backend/code-conventions.md +++ b/docs/backend/code-conventions.md @@ -17,7 +17,8 @@ internal/ # Private application code (HTTP server mode) │ ├── logger/ # Structured logging │ └── repository/ # Postgres repositories └── presentation/ # HTTP layer - ├── handler/ # Request handlers + ├── dto/ # Request/response shapes + domain→JSON mappers (never in handler/) + ├── handler/ # Request handlers (thin: bind DTO → service → DTO) └── middleware/ # Auth, JWT, inbox API key pkg/ # Public packages (embedded SDK + shared contracts) @@ -212,3 +213,53 @@ import ( ``` Use `goimports -local github.com/weprodev/wpd-message-gateway` to auto-format. + +## HTTP DTOs (presentation layer) + +**Never** define request/response structs in `handler/*.go`. HTTP JSON shapes belong in **`internal/presentation/dto/`** — one file per resource (e.g. `api_key.go`, `integration.go`). + +| Do | Don't | +| -- | ----- | +| `dto.CreateAPIKeyRequest` in handler bind | `type createAPIKeyBody struct` in handler | +| `dto.IntegrationFromDomain(intg)` | `integrationToJSON` helper in handler | +| Exported PascalCase names (`LoginRequest`) | Unexported `*Body` suffix types in handler | +| Pass `domain.*` to services/repositories | Pass `dto.*` to services or repositories | + +### Data flow (DTOs stop at the handler boundary) + +Repositories and port interfaces speak **domain only** — never presentation DTOs. + +``` +Inbound: JSON → handler.Bind(dto.Request) → req.ToDomain() → domain.* → Service → Port → Repository +Outbound: Repository → domain.* → Service → handler → dto.XFromDomain(domain.*) → JSON +Partial: JSON → dto.PatchRequest → req.ToPatch() → service.*Patch → Service (load domain, apply, save) +``` + +| Layer | Types it uses | Maps how | +| ----- | ------------- | -------- | +| **Handler** | `dto.*` | Bind JSON; call `ToDomain` / `ToPatch`; call `XFromDomain` before `c.JSON` | +| **Service** | `domain.*`, `service.*Patch` | Business rules; orchestrates ports | +| **Port / Repository** | `domain.*` | Postgres adapters scan rows ↔ domain structs | +| **Infrastructure** | `domain.*` | No `json` tags required; no import of `presentation/dto` | + +**Never send DTOs to a repository.** If a handler has a `dto.CreateTemplateRequest`, call `body.ToDomain(wid)` first, then pass the resulting `*domain.Template` to `PortalService.CreateTemplate`, which passes it to the template repository port. The repository interface looks like: + +```go +Create(ctx context.Context, t *domain.Template) error // port — domain only +``` + +Gateway `/v1/*` handlers may bind **`pkg/contracts`** types directly — those are the public SDK message API, not portal DTOs. + +### Mapping convention (idiomatic Go) + +| Direction | Style | Example | +| --------- | ----- | ------- | +| Domain → response DTO | Package func `{Type}FromDomain` | `dto.APIKeyPublicFromDomain(k)` | +| Request DTO → domain (create/upsert) | Value-receiver method `ToDomain()` | `body.ToDomain(wid)` | +| Request DTO → partial update | Value-receiver method `ToPatch()` | `body.ToPatch()` → `service.WorkspacePatch` | + +Use **`{Type}FromDomain`** package functions for outbound mapping (not methods on a zero-value receiver). Use **`ToDomain()`** on request DTOs when the result is a **`domain.*`** entity for persistence. Use **`ToPatch()`** when the result is a **service patch command** (service loads domain, applies fields, saves via repository). + +**Not infrastructure:** DTOs are presentation concerns (JSON field names, omitempty). `internal/infrastructure/` maps DB rows to **domain**, not HTTP. + +New or changed portal endpoints: add/update types in `dto/` and colocate mapper tests (`dto/*_test.go`) when mapping logic is non-trivial. diff --git a/docs/backend/e2e-testing.md b/docs/backend/e2e-testing.md index b7a8b7bf..d8c149c7 100644 --- a/docs/backend/e2e-testing.md +++ b/docs/backend/e2e-testing.md @@ -17,7 +17,7 @@ Use the Message Gateway to capture and verify all messages your app sends during ## Architecture ``` -Your App Gateway (memory_only) Test +Your App Gateway (memory dispatch) Test │ │ │ │ POST /v1/email ─────────▶│ captured in RAM │ │ POST /v1/email ─────────▶│ captured in RAM │ @@ -39,7 +39,7 @@ The inbox is protected. Every request to `/api/v1/workspaces/:wid/inbox/*` must 2. **Workspace membership** — user must be a member of the workspace 3. **Workspace API key** — `X-Api-Client-Id` + `X-Api-Client-Secret` matching the workspace -The **send** endpoint (`POST /v1/*`) uses API key + `X-Workspace-Key` (slug, not UUID). +The **send** endpoint (`POST /v1/*`) uses API key + `X-Workspace-Key` (workspace UUID preferred; slug also accepted). The **inbox** endpoint uses the workspace **UUID** in the URL path. --- @@ -104,7 +104,7 @@ KEY_JSON=$(curl -sS -X POST "$BASE/workspaces/$WORKSPACE_ID/api-keys" \ API_CLIENT_ID=$(echo "$KEY_JSON" | jq -r .client_id) API_CLIENT_SECRET=$(echo "$KEY_JSON" | jq -r .client_secret) -# 4) Dispatch mode defaults to memory_only — nothing to configure +# 4) Dispatch defaults to memory — nothing to configure echo "PORTAL_JWT=$PORTAL_JWT" >> $GITHUB_ENV echo "WORKSPACE_ID=$WORKSPACE_ID" >> $GITHUB_ENV echo "WORKSPACE_UK=$UK" >> $GITHUB_ENV @@ -303,7 +303,7 @@ func TestSignupEmail(t *testing.T) { ```bash curl -X POST http://localhost:10101/v1/email \ -u "client_id:client_secret" \ - -H "X-Workspace-Key: your_workspace_unique_key" \ + -H "X-Workspace-Key: " \ -H "Content-Type: application/json" \ -d '{"to":["user@example.com"],"subject":"Hello","html":"

World

"}' ``` diff --git a/docs/backend/portal-inbox.md b/docs/backend/portal-inbox.md index fb2beef0..61bd14d2 100644 --- a/docs/backend/portal-inbox.md +++ b/docs/backend/portal-inbox.md @@ -9,24 +9,27 @@ The Portal is the web UI and REST surface for the WPD Message Gateway when runni ### Portal UI (http://localhost:10104) | Feature | Description | -|---------|-------------| +| ------- | ------------- | | **Authentication** | Register and sign in (email + password → JWT) | -| **Workspaces** | List workspaces you belong to; open a workspace dashboard | +| **Workspaces** | List, create, and join workspaces | +| **Message logs** | Outbound send request audit trail (Overview + per-channel tabs) | +| **Email inbox** | Browse captured outbound emails with live SSE updates | +| **Email templates** | Create and manage reusable email layouts | | **Integrations** | Connect, activate, deactivate, or remove messaging providers | -| **Message logs** | Outbound send **request audit trail** (not memory inbox capture) | -| **Send test** | Send a test message per channel from the dashboard | +| **Settings** | General workspace settings, API keys, message dispatch mode | +| **Get started** | Curl examples for sending via the public gateway API | -No Portal pages yet for: workspace create, API keys, templates, members, settings, or memory inbox browsing. +Team member management is a settings placeholder. SMS, push, and chat show **request logs** in the UI; captured-message browsing for those channels is API-only. ### REST API (server) | Feature | Portal UI | Description | -|---------|:---------:|-------------| -| **Inbox capture** | | Messages stored in RAM (`memory_only` / `memory_and_provider`) — [Inbox API](#inbox-api-reference) | +| ------- | :-------: | ----------- | +| **Inbox capture** | Email ✓ | Messages captured when `message_dispatch_mode=memory` or `store_message_content=true` — [Inbox API](#inbox-api-reference) | | **Internal ingest** | | Automation writes to inbox (requires Portal auth) | -| **Workspace provisioning** | | Create workspace, API keys, settings — curl/Bruno/CI only; see [E2E bootstrap](./e2e-testing.md) | +| **Workspace API** | Partial | Create/join workspaces and manage keys in the UI; additional endpoints in [usage](./usage.md) | -Provider credentials are stored in PostgreSQL (encrypted at rest) and managed via the Portal **Integrations** page. Dispatch mode is REST-only (`PATCH /api/v1/workspaces/:wid/settings`). +Provider credentials are stored in PostgreSQL (encrypted at rest) and managed on the Portal **Integrations** page. Dispatch settings are configured in **Settings → Message Dispatch** or via REST. --- @@ -44,12 +47,12 @@ Register (email + password), then sign in. ## Message Inbox (Memory Capture) -When a workspace's **dispatch mode** is `memory_only` or `memory_and_provider`, outbound messages are captured in-process RAM and displayed in the Portal inbox. +When a workspace uses `message_dispatch_mode=memory`, outbound messages are captured in-process RAM and displayed in the Portal inbox. ``` Your App │ - │ POST /v1/email (workspace: memory_only) + │ POST /v1/email (workspace: memory dispatch) ▼ Memory Provider └──────────────────▶ Portal Inbox (REST + SSE) @@ -67,26 +70,29 @@ Memory Provider --- -## Dispatch Modes +## Dispatch & Inbox Capture -Each workspace controls how its outbound messages are handled: +Each workspace controls routing and inbox capture with two settings: -| `message_dispatch_mode` | Behavior | -|------------------------|----------| -| `memory_only` | Captured in RAM only, **no** external provider called. **Default.** | -| `provider_only` | Sent through the connected integration, **no** memory copy. | -| `memory_and_provider` | Stored in memory **and** sent through the integration. | +| Setting | Values | Role | +|---------|--------|------| +| `message_dispatch_mode` | `memory` (default) \| `provider` | Routing target | +| `store_message_content` | `false` (default) \| `true` | Capture message body in portal inbox when enabled | -Set via REST (no Portal UI page yet): +Portal **Settings → Message Dispatch**: radio **Memory** / **Provider**, plus a checkbox for **Store message content in inbox** (see [usage](./usage.md)). + +Set via Portal **Settings → Message Dispatch** or REST: ```bash PATCH /api/v1/workspaces/:wid/settings Content-Type: application/json Authorization: Bearer -{ "message_dispatch_mode": "provider_only" } +{ "message_dispatch_mode": "provider", "store_message_content": "false" } ``` +Request logs (`message_request_logs`) are always written for operational tracing. + --- ## Authentication (Inbox & API) @@ -186,8 +192,8 @@ portal: ui_port: 10104 # React dev server (Portal UI) ``` -> **Provider credentials** (Mailgun API keys, etc.) are **not** in this file. -> They are stored AES-encrypted in PostgreSQL and configured via the **Portal REST API** (no Portal UI page yet). +> **Provider credentials** (Mailgun API keys, etc.) are **not** in this file. +> Configure them on the Portal **Integrations** page or via the integrations REST API. Values are stored AES-encrypted in PostgreSQL. --- diff --git a/docs/backend/usage.md b/docs/backend/usage.md index c17c0ae9..06b68b6e 100644 --- a/docs/backend/usage.md +++ b/docs/backend/usage.md @@ -10,7 +10,7 @@ │ go get .../wpd-message-gateway │ git clone → make start │ │ gateway.New(config) │ POST /v1/email (HTTP) │ │ Config lives in your code │ Config lives in PostgreSQL │ -│ No DB required │ React Portal UI (partial) │ +│ No DB required │ React Portal UI │ └──────────────────────────────────┴──────────────────────────────────┘ ``` @@ -141,22 +141,28 @@ Open **http://localhost:10104** — the Portal UI. ### First time setup (Portal UI) -The Portal UI currently supports: +The Portal UI supports: 1. **Register** and **sign in** (email + password) -2. **Workspaces** — list workspaces you belong to and open one +2. **Workspaces** — list, create, and join workspaces 3. **Integrations** — connect, activate, deactivate, or remove messaging providers 4. **Message logs** — audit trail of gateway send requests (Overview + channel tabs) -5. **Send test** — send a test message through the gateway for the workspace +5. **Email inbox** — browse captured outbound emails (memory dispatch or `store_message_content`) +6. **Email templates** — create and manage reusable email layouts +7. **Settings** — general workspace settings, API keys, and message dispatch mode + +Use the **Get started** dialog on the logs pages for curl examples. Send traffic through the public gateway API (`POST /v1/email`, `POST /v1/sms`, etc.) with a workspace API key. For local dev with PostgreSQL, run migrations then apply seeds in order (see `database/init-db.sh`): `001_seed_permissions.sql`, `002_seed_providers.sql`, `003_seed_mailgun_config.sql`, and `004_demo_workspace.sql` (optional demo data: `demo@weprodev.com` / `secret`). ### Sending via HTTP +`X-Workspace-Key` accepts the workspace **UUID** (recommended — copy from Settings or the Get started dialog) or the workspace slug. + ```bash curl -X POST http://localhost:10101/v1/email \ -u "wk_abc123:your-secret" \ - -H "X-Workspace-Key: myapp" \ + -H "X-Workspace-Key: 550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{ "to": ["user@example.com"], @@ -170,7 +176,7 @@ Or with Bearer token: ```bash curl -X POST http://localhost:10101/v1/email \ -H "Authorization: Bearer wk_abc123:your-secret" \ - -H "X-Workspace-Key: myapp" \ + -H "X-Workspace-Key: 550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{"to": ["user@example.com"], "subject": "Hello", "html": "

World

"}' ``` @@ -183,7 +189,7 @@ import requests response = requests.post( "http://localhost:10101/v1/email", auth=("wk_abc123", "your-secret"), - headers={"X-Workspace-Key": "myapp"}, + headers={"X-Workspace-Key": "550e8400-e29b-41d4-a716-446655440000"}, json={ "to": ["user@example.com"], "subject": "Hello from Python", @@ -200,7 +206,7 @@ const response = await fetch("http://localhost:10101/v1/email", { method: "POST", headers: { "Authorization": "Basic " + btoa("wk_abc123:your-secret"), - "X-Workspace-Key": "myapp", + "X-Workspace-Key": "550e8400-e29b-41d4-a716-446655440000", "Content-Type": "application/json", }, body: JSON.stringify({ @@ -240,10 +246,10 @@ curl -H "Authorization: Bearer " http://localhost:10101/api/v1/workspaces Use your **workspace API key** credentials: ```bash # Basic auth -curl -u "wk_abc123:secret" -H "X-Workspace-Key: myapp" ... +curl -u "wk_abc123:secret" -H "X-Workspace-Key: " ... # Bearer (colon-separated) -curl -H "Authorization: Bearer wk_abc123:secret" -H "X-Workspace-Key: myapp" ... +curl -H "Authorization: Bearer wk_abc123:secret" -H "X-Workspace-Key: " ... ``` --- @@ -320,7 +326,7 @@ result, err := gw.SendChat(ctx, &contracts.ChatMessage{ ### Portal REST API (JWT auth) -Routes registered in `internal/presentation/router.go`. **Portal UI pages exist only for auth, workspace list, message logs, and send test** — other management routes are REST-only until UI is built. +Routes registered in `internal/presentation/router.go`. **Portal UI pages exist for auth, workspace list, message logs, email inbox, integrations, and settings** — other management routes are REST-only until UI is built. #### Auth @@ -337,11 +343,8 @@ Routes registered in `internal/presentation/router.go`. **Portal UI pages exist |--------|----------|:---------:|-------------| | GET | `/api/v1/workspaces` | ✓ | List workspaces for the logged-in user | | GET | `/api/v1/workspaces/:wid/logs` | ✓ | Message request logs (optionally filter by `channel`) | -| POST | `/api/v1/workspaces/:wid/send-test/:channel` | ✓ | Send test via gateway (`email` / `sms` / `push` / `chat`) | - -#### Workspace provisioning & management (REST only — no Portal UI) -Used by Bruno, curl, and CI bootstrap. Not documented endpoint-by-endpoint here; see `internal/presentation/router.go` and [E2E testing](./e2e-testing.md) for create workspace, API keys, settings, integrations, templates, and members. +Additional workspace endpoints (create, API keys, settings, templates, members, inbox) are implemented in `internal/presentation/router.go` and covered in [Portal inbox](./portal-inbox.md) and [E2E testing](./e2e-testing.md). ### Workspace access (portal JWT routes & RBAC) @@ -349,7 +352,7 @@ Workspace-scoped routes require a valid portal JWT and are governed by a Role-Ba Roles and permissions are defined in [permission.go](../../internal/core/domain/permission.go): - **admin**: Full read/write access. The creator of a workspace is automatically assigned this role (as `admin` in both members repository and `gogate`). -- **member**: Read-only access to workspaces, members, API keys, logs, integrations, templates, settings, and invitations, plus the ability to send test messages (`send.test`). +- **member**: Read-only access to workspaces, members, API keys, logs, integrations, templates, settings, and invitations, plus inbox mutations (`inbox.write`). To bootstrap roles and permissions, make sure you run the database seeds: 1. Run `database/seeds/001_seed_permissions.sql` to populate roles (`admin`, `member`), default permissions, and role-to-permission mappings. @@ -364,9 +367,9 @@ The HTTP server uses two distinct authorization approaches by design: - **Inbox SSE API (`/api/v1/workspaces/:wid/inbox/...`)**: Restricts access using JWT tokens combined with a direct database workspace membership check (`RequireWorkspaceMember`) and the workspace API key (`RequireWorkspaceAPIKey`). This decoupled model allows client-side SDKs, SSE event streaming, and automation runners to interact with the simulated inbox without requiring full portal RBAC configuration. -### Inbox API (captured messages — REST / Bruno, not Portal UI) +### Inbox API -The Portal UI shows **request logs** (`/logs`), not the memory **inbox** (`/inbox/*`). Inbox capture, SSE, and internal ingest are documented in [Portal inbox](./portal-inbox.md). +Captured messages are available through the Portal **Email** page (UI + SSE) and the REST inbox endpoints documented in [Portal inbox](./portal-inbox.md). SMS, push, and chat channels expose request **logs** in the UI; captured-message browsing for those channels is API-only today. --- @@ -386,8 +389,8 @@ portal: ui_port: 10104 ``` -> **Note:** Provider credentials (Mailgun API keys, etc.) are **NOT** in YAML files. -> They are configured via the Portal REST API and stored encrypted in PostgreSQL. +> **Note:** Provider credentials (Mailgun API keys, etc.) are **not** in YAML files. +> Configure them on the Portal **Integrations** page or via `POST /api/v1/workspaces/:wid/integrations`. Values are stored encrypted in PostgreSQL. ### Environment Variables (Server) @@ -410,27 +413,29 @@ MESSAGE_CONFIG_ENCRYPTION_KEY=your-32-byte-key-here --- -## Message Dispatch Modes +## Message Dispatch & Content Storage -Control how messages are handled per workspace. Default is `memory_only`. +Control how messages are routed and whether bodies are captured in the portal inbox. -Set via REST (no Portal UI page yet): +Defaults: `message_dispatch_mode=memory`, `store_message_content=false`. + +Invalid values for dispatch settings return **400 Bad Request** on PATCH. + +Set via Portal **Settings → Message Dispatch** or REST: ```bash PATCH /api/v1/workspaces/:wid/settings Authorization: Bearer Content-Type: application/json -{ "message_dispatch_mode": "provider_only" } +{ "message_dispatch_mode": "provider", "store_message_content": "true" } ``` -See [Portal inbox](./portal-inbox.md) for mode behavior. +Portal UI: select **Memory** or **Provider**, then check **Store message content in inbox** on or off. + +See [Portal inbox](./portal-inbox.md) for routing behavior. -| Mode | Behavior | Use Case | -|------|----------|----------| -| `memory_only` | In-process RAM only, **not sent** | Development, testing | -| `provider_only` | Sent to real provider, no local copy | Production | -| `memory_and_provider` | Both — local copy + real send | Staging, debugging | +Every send also creates a `message_request_logs` row for operational tracing (correlation via `request_id`). --- @@ -445,11 +450,11 @@ Using a Mailgun Sandbox Domain: ### Provider Not Sending -Check workspace `message_dispatch_mode` via `GET /api/v1/workspaces/:wid/settings`. If it is `memory_only`, messages are captured locally — not sent to the provider. +Check workspace settings via `GET /api/v1/workspaces/:wid/settings`. If `message_dispatch_mode` is `memory`, messages are captured locally — not sent to the provider. ### API Key Rejected -- Ensure `X-Workspace-Key` matches the workspace's `unique_key` (slug, not UUID) +- Ensure `X-Workspace-Key` matches the workspace UUID or slug - Verify the API key is active (`GET /api/v1/workspaces/:wid/api-keys` with portal JWT) - Check `client_id` and `client_secret` — secret is shown once at creation diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8c463035..3ce708e2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@radix-ui/react-checkbox": "^1.3.6", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", @@ -398,9 +399,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", - "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", "dev": true, "funding": [ { @@ -442,9 +443,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.8.tgz", - "integrity": "sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", "dev": true, "funding": [ { @@ -458,7 +459,7 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^6.0.2", + "@csstools/color-helpers": "^6.1.0", "@csstools/css-calc": "^3.2.1" }, "engines": { @@ -493,9 +494,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", - "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", "dev": true, "funding": [ { @@ -1314,14 +1315,14 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", - "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.2" + "@tybys/wasm-util": "^0.10.3" }, "funding": { "type": "github", @@ -1709,9 +1710,9 @@ } }, "node_modules/@oxc-resolver/binding-android-arm-eabi": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.3.tgz", - "integrity": "sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.23.0.tgz", + "integrity": "sha512-8IJyWRLVAyhTfe9/TIEbQqSQnl5rUqYJrUOS6Dkr+Mq9FGHMxDGeiEmwkBqCvDP5KckpPh/GYSgbag66O6JsCw==", "cpu": [ "arm" ], @@ -1723,9 +1724,9 @@ ] }, "node_modules/@oxc-resolver/binding-android-arm64": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.3.tgz", - "integrity": "sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.23.0.tgz", + "integrity": "sha512-pprVojnNhHxupwTT2gdeUlkxll6XEvWWBk3oVicOSNVWQC99OBnDhMQDoirqnzrE1bScQSMS2JgPpqdlrhz/Fg==", "cpu": [ "arm64" ], @@ -1737,9 +1738,9 @@ ] }, "node_modules/@oxc-resolver/binding-darwin-arm64": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.3.tgz", - "integrity": "sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.23.0.tgz", + "integrity": "sha512-mbIrWIMAJeytyee36OyUP5XH92TP7FaKaQ2m5AjokKy7STgjrhRt7SMXqpqLjhGm6Xn721Xmsg6H3Rtd9YQETw==", "cpu": [ "arm64" ], @@ -1751,9 +1752,9 @@ ] }, "node_modules/@oxc-resolver/binding-darwin-x64": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.3.tgz", - "integrity": "sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.23.0.tgz", + "integrity": "sha512-UnIphmZ1LazUCr9DXWaKYWtKDefPMbgLsywaoYxRqVCNHhq4MM6d2q1Nz1i9Vzxt5i+cE2nRUYpAUHr/lijNYA==", "cpu": [ "x64" ], @@ -1765,9 +1766,9 @@ ] }, "node_modules/@oxc-resolver/binding-freebsd-x64": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.3.tgz", - "integrity": "sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.23.0.tgz", + "integrity": "sha512-aaZ/cSEYFkSxgS2hOrobT6RQcsWNviOX8dW6CEkVx2/UYkAf9MeHbjl3W0usWV53rVV//ndBdn2nb1y7jsu4lw==", "cpu": [ "x64" ], @@ -1779,9 +1780,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.3.tgz", - "integrity": "sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.23.0.tgz", + "integrity": "sha512-IoJLvO5SjLSVMaq83BNTrPCb1FppvoJc1IhZ5CoUVl3PykUBku7D+LK1j0GSurhJcIc6zfjghsvaZNpq5ev6Mg==", "cpu": [ "arm" ], @@ -1793,9 +1794,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.3.tgz", - "integrity": "sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.23.0.tgz", + "integrity": "sha512-vskFpwg44T/LFsfjSCnVZ5ygcuqzPC1yUzVEiKa8BgHAQz0+QLQQW3EGWLPVi8EXFghzjR4EtgPBtOhCjU4jdw==", "cpu": [ "arm" ], @@ -1807,9 +1808,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.3.tgz", - "integrity": "sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.23.0.tgz", + "integrity": "sha512-//TcHVhrChyw5RYtgts6WO7KcWq9387c1Z5Zvhqpk/ktAbyaRYgBZrpSY1GDCFq50ASt6B6jhh+JxB1rB45IAg==", "cpu": [ "arm64" ], @@ -1824,9 +1825,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm64-musl": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.3.tgz", - "integrity": "sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.23.0.tgz", + "integrity": "sha512-ZFqlwiTf7CXLLSGyAR9tYiO33LiaeIEXW+xm42d8mnUGpDgPltyrCGYtQezyMMEXvjhOgCz1X+i7sbDTJEx+bg==", "cpu": [ "arm64" ], @@ -1841,9 +1842,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.3.tgz", - "integrity": "sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.23.0.tgz", + "integrity": "sha512-oZ5LeN5+H1R19dRjTAxKrxQguH+AsemHcnthEfFxf4OjmBSty2doHLeSmMunKy3zpTHJQ3lh3Af+dNS+W6dYeA==", "cpu": [ "ppc64" ], @@ -1858,9 +1859,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.3.tgz", - "integrity": "sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.23.0.tgz", + "integrity": "sha512-O4ciFDyX5ebQd0qkb1bjAIg8IEfiLT03GbSeylwlwlUMK9KwBWaALwrxSbc0Msaz4U6iPj+T9eRXpD5mxBfmvA==", "cpu": [ "riscv64" ], @@ -1875,9 +1876,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.3.tgz", - "integrity": "sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.23.0.tgz", + "integrity": "sha512-P3o8Y9kISYjcxadmbO+94ThRwLhwGuDAbA7dcdd4+YLpfeF+mmobz8fXf4NmSdfSqjyRSkceJDBRZha9NVYkiQ==", "cpu": [ "riscv64" ], @@ -1892,9 +1893,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.3.tgz", - "integrity": "sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.23.0.tgz", + "integrity": "sha512-oj03m1E3RmTFczKhcKJDzHaEDKJnPIsDcQFVxBJsSdXGSuIPdt5TvcM332FfMQgzI6yDJqyl4InrnFfXrmUTKQ==", "cpu": [ "s390x" ], @@ -1909,9 +1910,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-x64-gnu": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.3.tgz", - "integrity": "sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.23.0.tgz", + "integrity": "sha512-BqJxbSC8FdP7mSuSpRePTGHm0hXWV+dfz//f7SjsteZncLaBgWTBmi/OZNv7sX6CyG/Pt/eJkPorP+DkMOhMwQ==", "cpu": [ "x64" ], @@ -1926,9 +1927,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-x64-musl": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.3.tgz", - "integrity": "sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.23.0.tgz", + "integrity": "sha512-utmw+VmUrW4K8LI5/6jhg4aGYKJHOIjQ9syYOOA6pF3w7haKu4r4enTe2U0C04/HbUvkq/Zif43xFsKW1Pnq9w==", "cpu": [ "x64" ], @@ -1943,9 +1944,9 @@ ] }, "node_modules/@oxc-resolver/binding-openharmony-arm64": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.3.tgz", - "integrity": "sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.23.0.tgz", + "integrity": "sha512-V6lbRrthHa4TbvsLjPtg+EkXT1tRY+s4I8rYLXUfiHlZzGx3sLv1EH9CEOOevjvUYHLsbe/gqCIc73XnQfPb9A==", "cpu": [ "arm64" ], @@ -1957,9 +1958,9 @@ ] }, "node_modules/@oxc-resolver/binding-wasm32-wasi": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.3.tgz", - "integrity": "sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.23.0.tgz", + "integrity": "sha512-gRoOxQPdnAmIAjxcuQNBxfihvx+wjTaQM/9/eP12xwnGNawOG/+Zz9RHN4WNSxT45b5CrscK4NB8aPh+oZQXAQ==", "cpu": [ "wasm32" ], @@ -1967,18 +1968,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.11.0", - "@emnapi/runtime": "1.11.0", - "@napi-rs/wasm-runtime": "^1.1.5" + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", - "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, @@ -1988,9 +1989,9 @@ } }, "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", - "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dev": true, "license": "MIT", "optional": true, @@ -2010,9 +2011,9 @@ } }, "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.3.tgz", - "integrity": "sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.23.0.tgz", + "integrity": "sha512-CgTGMYsJVe1eUiCdJTpGw21svXw79ITsemN1h0hcNkiswasDbN5MoibSLY+gRMWP5syfEz5iffrjZnwEP8xeUA==", "cpu": [ "arm64" ], @@ -2024,9 +2025,9 @@ ] }, "node_modules/@oxc-resolver/binding-win32-x64-msvc": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.3.tgz", - "integrity": "sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.23.0.tgz", + "integrity": "sha512-gUGJpr+Rn6zMxm5juApV0K3U845i8t47o8k+rbO0BHbi4PoJIfSPeQmrE2dgohQm2g5k6iviNFyXCGqvmaYUpw==", "cpu": [ "x64" ], @@ -2043,15 +2044,45 @@ "integrity": "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==", "license": "MIT" }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.6.tgz", + "integrity": "sha512-eUEUoGMDpfkgHWSE97ZZaUJtzR1M7EKnNIpD1Q16+8JR9NWghcaqMulx9PuCQ720w0UclfYn6FEbCdd5Hx087g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-use-size": "1.1.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-collection": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.10.tgz", - "integrity": "sha512-IVVz4EvBcKjrzKgof714qDnz/SzQAkLA2Emh5edlHbgcE6fNd3Un6CJLlaYcnm8N4JmAtzQgse4dOKxcD2yc9g==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.11.tgz", + "integrity": "sha512-djW9+zeg137KQdlPtmE8xnaD+K2rcXXMWFrSg0hsmYZ6HRbdTA7tDHFgpaW9+huWVEu0RCabL+985T4TA0BE7g==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0" }, "peerDependencies": { @@ -2100,21 +2131,21 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.17.tgz", - "integrity": "sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.18.tgz", + "integrity": "sha512-apa28mldjMgORmE6g/w3sCcA0Y9UAVeeDVoozN4i7kOw12mLl9RBchfzK3Nn6qxOWjrZhK1Lfy7f07kyzxtnBw==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", - "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-dismissable-layer": "1.1.14", "@radix-ui/react-focus-guards": "1.1.4", - "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-focus-scope": "1.1.11", "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", @@ -2151,16 +2182,16 @@ } }, "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.13.tgz", - "integrity": "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.14.tgz", + "integrity": "sha512-4lUhWTWAjbDIqFrAPWJ3WqBOpO5YchVZ88X3nh6H9Lu5AFi5nCUeTPj3D8FSDmabmFeRe9ME0BDA4MwKTha5GQ==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-escape-keydown": "1.1.2" + "@radix-ui/react-use-effect-event": "0.0.3" }, "peerDependencies": { "@types/react": "*", @@ -2193,13 +2224,13 @@ } }, "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.10.tgz", - "integrity": "sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.11.tgz", + "integrity": "sha512-Mn88Vg2whaRocGJNOH+DKFqYm6ySFPQaiwHNxZPyjn99B52KAEJWWY9NP83+nWdk2HM3rdov+STu9AG471Rt9w==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { @@ -2236,12 +2267,12 @@ } }, "node_modules/@radix-ui/react-portal": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.12.tgz", - "integrity": "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.13.tgz", + "integrity": "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { @@ -2283,9 +2314,9 @@ } }, "node_modules/@radix-ui/react-primitive": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz", - "integrity": "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.7.tgz", + "integrity": "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==", "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.3.0" @@ -2306,18 +2337,18 @@ } }, "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.13.tgz", - "integrity": "sha512-9gkwneI0guf8JDmrFxPjJF6Ozzgioyw+/lonYNCwefS9ZHA05er0BVHiXr+LbWGHxUfczvMY6G1oiZZi1VzjRw==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.14.tgz", + "integrity": "sha512-8Qcnx9447tx/aCBgw6Jenfqg4Skq+vqab9mCBmuGNipIS5YXvL275wbKEu7+ICYHIlAPgCduUMJH1XOYewKF6Q==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.4", - "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-collection": "1.1.11", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3" }, @@ -2337,12 +2368,12 @@ } }, "node_modules/@radix-ui/react-separator": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.10.tgz", - "integrity": "sha512-Y6K6jLQCVfCnTL2MEtGxDLffkhNfEfHsEg3Wa8JU+IWdn3EWbLXd3OuOfQRN7p/W/cUce1WyTk3QeuAoDBzN9g==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.11.tgz", + "integrity": "sha512-jRhe86+8PF7VZ1u14eOWVOuh2BuAhALg/FT1VcMC4OHedMTRUazDnDlKTt+yxo5cRNKHMfmvZ4sSQtWDeMV4CQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.6" + "@radix-ui/react-primitive": "2.1.7" }, "peerDependencies": { "@types/react": "*", @@ -2378,9 +2409,9 @@ } }, "node_modules/@radix-ui/react-tabs": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.15.tgz", - "integrity": "sha512-kxc9gI6/HfcU4nfMMVS3AmQK414kbU1IE6UCJmMmxjhO3cRPXOyYnmvyKD+ODt7q56nRq9l7Wovi6uaGwKgMlg==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.16.tgz", + "integrity": "sha512-v3Ab2l7z6U7tRB4xA0IyKdq0OsqaO1o9ZjsIEoKKnSZ/l96mZz8aCTX0NCXw+YVHJXr8Km4d+Mn6/Q8YjXa+gw==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.4", @@ -2388,8 +2419,8 @@ "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.6", - "@radix-ui/react-primitive": "2.1.6", - "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-primitive": "2.1.7", + "@radix-ui/react-roving-focus": "1.1.14", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { @@ -2459,14 +2490,26 @@ } } }, - "node_modules/@radix-ui/react-use-escape-keydown": { + "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.2.tgz", - "integrity": "sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", + "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "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-use-previous": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.2.tgz", + "integrity": "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -2477,11 +2520,14 @@ } } }, - "node_modules/@radix-ui/react-use-layout-effect": { + "node_modules/@radix-ui/react-use-size": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", - "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz", + "integrity": "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==", "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -2493,9 +2539,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", "cpu": [ "arm64" ], @@ -2510,9 +2556,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", "cpu": [ "arm64" ], @@ -2527,9 +2573,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", "cpu": [ "x64" ], @@ -2544,9 +2590,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", "cpu": [ "x64" ], @@ -2561,9 +2607,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", "cpu": [ "arm" ], @@ -2578,9 +2624,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", "cpu": [ "arm64" ], @@ -2598,9 +2644,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", "cpu": [ "arm64" ], @@ -2618,9 +2664,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", "cpu": [ "ppc64" ], @@ -2638,9 +2684,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", "cpu": [ "s390x" ], @@ -2658,9 +2704,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", "cpu": [ "x64" ], @@ -2678,9 +2724,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", "cpu": [ "x64" ], @@ -2698,9 +2744,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", "cpu": [ "arm64" ], @@ -2715,9 +2761,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", "cpu": [ "wasm32" ], @@ -2725,30 +2771,41 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -2757,9 +2814,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", "cpu": [ "arm64" ], @@ -2774,9 +2831,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", "cpu": [ "x64" ], @@ -2945,14 +3002,13 @@ "license": "MIT" }, "node_modules/@storybook/icons": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-2.0.2.tgz", - "integrity": "sha512-KZBCpXsshAIjczYNXR/rlxEtCUX/eAbpFNwKi8bcOomrLA4t/SyPz5RF+lVPO2oZBUE4sAkt43mfJUevQDSEEw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-2.1.0.tgz", + "integrity": "sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==", "dev": true, "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@storybook/react": { @@ -3046,15 +3102,15 @@ } }, "node_modules/@swc/core": { - "version": "1.15.41", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.41.tgz", - "integrity": "sha512-03nQq/082QRJJiOvp3FGbgxTGyyxMxohPTjhk/W9bD2J0tk4ukITI7goOhOO2WbaHn/lsPmo/zf8+DIXhwpgYQ==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.43.tgz", + "integrity": "sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.26" + "@swc/types": "^0.1.27" }, "engines": { "node": ">=10" @@ -3064,18 +3120,18 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.41", - "@swc/core-darwin-x64": "1.15.41", - "@swc/core-linux-arm-gnueabihf": "1.15.41", - "@swc/core-linux-arm64-gnu": "1.15.41", - "@swc/core-linux-arm64-musl": "1.15.41", - "@swc/core-linux-ppc64-gnu": "1.15.41", - "@swc/core-linux-s390x-gnu": "1.15.41", - "@swc/core-linux-x64-gnu": "1.15.41", - "@swc/core-linux-x64-musl": "1.15.41", - "@swc/core-win32-arm64-msvc": "1.15.41", - "@swc/core-win32-ia32-msvc": "1.15.41", - "@swc/core-win32-x64-msvc": "1.15.41" + "@swc/core-darwin-arm64": "1.15.43", + "@swc/core-darwin-x64": "1.15.43", + "@swc/core-linux-arm-gnueabihf": "1.15.43", + "@swc/core-linux-arm64-gnu": "1.15.43", + "@swc/core-linux-arm64-musl": "1.15.43", + "@swc/core-linux-ppc64-gnu": "1.15.43", + "@swc/core-linux-s390x-gnu": "1.15.43", + "@swc/core-linux-x64-gnu": "1.15.43", + "@swc/core-linux-x64-musl": "1.15.43", + "@swc/core-win32-arm64-msvc": "1.15.43", + "@swc/core-win32-ia32-msvc": "1.15.43", + "@swc/core-win32-x64-msvc": "1.15.43" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -3087,9 +3143,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.41", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.41.tgz", - "integrity": "sha512-kREh6J5paQFvP3i7f/4FbqRNOJREutVFVOkder4GVyCBQ39YmER55cW/y1NNjwrchzFqgYswFn0mMDCqbqKzrw==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.43.tgz", + "integrity": "sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==", "cpu": [ "arm64" ], @@ -3104,9 +3160,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.15.41", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.41.tgz", - "integrity": "sha512-N8B56ESFazZAWZyIkecADSPCwlLEinW7QLMEeotCpv4J7VXwfH+OLkmRL8o96UZ+1355fwHxDTS6/wK7yucvkA==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.43.tgz", + "integrity": "sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==", "cpu": [ "x64" ], @@ -3121,9 +3177,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.41", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.41.tgz", - "integrity": "sha512-6XrId2fyle0mS5xxON8rU84mPd2Cq1kDJRj+4BnQKTd7u+2kSA6Ww+JkOP0iTNqOqt9OXhPOEAjBHAuonWcdCg==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.43.tgz", + "integrity": "sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==", "cpu": [ "arm" ], @@ -3138,9 +3194,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.41", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.41.tgz", - "integrity": "sha512-ynLIarxlkVnqHn1D0fKOVht6mNU5ks6lrH+MY3kkS+XFaGGgDxFZVjWKJlkYTKm3RCvBTfA8Ng5fLufXheMRKQ==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.43.tgz", + "integrity": "sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==", "cpu": [ "arm64" ], @@ -3158,9 +3214,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.41", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.41.tgz", - "integrity": "sha512-dXu/5vd4gh8symyhRF+4G7gOPkjmb4pONhh7sl+6GSiW0LOKZlfu5kXmyFbTz9smOT7jgr002qY9b1nujjXt2A==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.43.tgz", + "integrity": "sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==", "cpu": [ "arm64" ], @@ -3178,9 +3234,9 @@ } }, "node_modules/@swc/core-linux-ppc64-gnu": { - "version": "1.15.41", - "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.41.tgz", - "integrity": "sha512-XGO6zVPXoPE0gf/XnI4jBbafNT13AYgoh6ns0JCSdOetI/kqVf0vhpz7NuNgAzZrMVCsmieqjPoTwViDgh4mOQ==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.43.tgz", + "integrity": "sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==", "cpu": [ "ppc64" ], @@ -3198,9 +3254,9 @@ } }, "node_modules/@swc/core-linux-s390x-gnu": { - "version": "1.15.41", - "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.41.tgz", - "integrity": "sha512-0WUglRwyZtW+iMi7J3iFdrCxreZZIKf4egTwEQfIYRsqFax69A0OrFj+NIoFSE03xBT/IFRrg+S8K6f9Ky+4hA==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.43.tgz", + "integrity": "sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==", "cpu": [ "s390x" ], @@ -3218,9 +3274,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.41", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.41.tgz", - "integrity": "sha512-VxkuQK59c0tHm6uJZCUrS3cyA2JhGGfdU6e41SZz0x/JS+4Sm7C1mIc97In14vkZJopEt7yXA2TouCqZDSygEA==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.43.tgz", + "integrity": "sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==", "cpu": [ "x64" ], @@ -3238,9 +3294,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.41", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.41.tgz", - "integrity": "sha512-/0qXIu1ZxggLuovLb22vFfKHq2AA4n6Whw5UwmVCHk4pkw7KWnPIQpMCEqUMPsNkFJig7PPp/TSYFu8ZEb2rtQ==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.43.tgz", + "integrity": "sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==", "cpu": [ "x64" ], @@ -3258,9 +3314,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.41", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.41.tgz", - "integrity": "sha512-Y481sMNZM6rECh9VO4+y26N1lWEDAyxnBZskUf37fl90uHE946VHfmiVQWT0uMFOhyJJFovGTRuF4W82dwewUg==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.43.tgz", + "integrity": "sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==", "cpu": [ "arm64" ], @@ -3275,9 +3331,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.41", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.41.tgz", - "integrity": "sha512-BAchBD5qeUzy3hiPSLJtaaoSm4blCLyYffOF1bGE4ETcV+OisqjUAwDQMJj++4bTpvMCDzwC+Bj3PmQyBCtscw==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.43.tgz", + "integrity": "sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==", "cpu": [ "ia32" ], @@ -3292,9 +3348,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.41", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.41.tgz", - "integrity": "sha512-WOkA+fJ/ViVBQDsSV9JC52NACTe5PhlurA6viASDZGb7HR3KS01ZG7RZ+Bg6SVQFIoq3gSbTsskQVe6EbHFAYw==", + "version": "1.15.43", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.43.tgz", + "integrity": "sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==", "cpu": [ "x64" ], @@ -3326,9 +3382,9 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", - "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", + "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", "dev": true, "license": "MIT", "dependencies": { @@ -3338,37 +3394,37 @@ "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.3.1" + "tailwindcss": "4.3.2" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz", - "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", + "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", "dev": true, "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.1", - "@tailwindcss/oxide-darwin-arm64": "4.3.1", - "@tailwindcss/oxide-darwin-x64": "4.3.1", - "@tailwindcss/oxide-freebsd-x64": "4.3.1", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", - "@tailwindcss/oxide-linux-x64-musl": "4.3.1", - "@tailwindcss/oxide-wasm32-wasi": "4.3.1", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" + "@tailwindcss/oxide-android-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-x64": "4.3.2", + "@tailwindcss/oxide-freebsd-x64": "4.3.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-x64-musl": "4.3.2", + "@tailwindcss/oxide-wasm32-wasi": "4.3.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz", - "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", + "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", "cpu": [ "arm64" ], @@ -3383,9 +3439,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz", - "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", + "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", "cpu": [ "arm64" ], @@ -3400,9 +3456,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz", - "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", + "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", "cpu": [ "x64" ], @@ -3417,9 +3473,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz", - "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", + "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", "cpu": [ "x64" ], @@ -3434,9 +3490,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz", - "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", + "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", "cpu": [ "arm" ], @@ -3451,9 +3507,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz", - "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", + "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", "cpu": [ "arm64" ], @@ -3471,9 +3527,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz", - "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", + "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", "cpu": [ "arm64" ], @@ -3491,9 +3547,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz", - "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", + "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", "cpu": [ "x64" ], @@ -3511,9 +3567,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz", - "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", + "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", "cpu": [ "x64" ], @@ -3531,9 +3587,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz", - "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", + "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -3549,9 +3605,9 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.10.0", - "@emnapi/runtime": "^1.10.0", - "@emnapi/wasi-threads": "^1.2.1", + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" @@ -3561,9 +3617,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", - "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", + "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", "cpu": [ "arm64" ], @@ -3578,9 +3634,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz", - "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", + "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", "cpu": [ "x64" ], @@ -3595,23 +3651,23 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.1.tgz", - "integrity": "sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.2.tgz", + "integrity": "sha512-rjVWYCa7Ngbi5AarT6k8TkxUG3Wl1QKzHdIZVsjZSzf36Jmo2IKZt/NHRAwly8oDkbBOH0YTu+CHuf9jPxMc+g==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.3.1", - "@tailwindcss/oxide": "4.3.1", - "postcss": "8.5.15", - "tailwindcss": "4.3.1" + "@tailwindcss/node": "4.3.2", + "@tailwindcss/oxide": "4.3.2", + "postcss": "^8.5.15", + "tailwindcss": "4.3.2" } }, "node_modules/@tanstack/query-core": { - "version": "5.101.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz", - "integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==", + "version": "5.101.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.2.tgz", + "integrity": "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==", "license": "MIT", "funding": { "type": "github", @@ -3619,12 +3675,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.101.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz", - "integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==", + "version": "5.101.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.2.tgz", + "integrity": "sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.101.0" + "@tanstack/query-core": "5.101.2" }, "funding": { "type": "github", @@ -3724,9 +3780,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, @@ -3877,17 +3933,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz", - "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz", + "integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.61.1", - "@typescript-eslint/type-utils": "8.61.1", - "@typescript-eslint/utils": "8.61.1", - "@typescript-eslint/visitor-keys": "8.61.1", + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/type-utils": "8.62.1", + "@typescript-eslint/utils": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -3900,7 +3956,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.61.1", + "@typescript-eslint/parser": "^8.62.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -3916,16 +3972,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz", - "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz", + "integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.61.1", - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/typescript-estree": "8.61.1", - "@typescript-eslint/visitor-keys": "8.61.1", + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", "debug": "^4.4.3" }, "engines": { @@ -3941,14 +3997,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz", - "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz", + "integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.61.1", - "@typescript-eslint/types": "^8.61.1", + "@typescript-eslint/tsconfig-utils": "^8.62.1", + "@typescript-eslint/types": "^8.62.1", "debug": "^4.4.3" }, "engines": { @@ -3963,14 +4019,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz", - "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz", + "integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/visitor-keys": "8.61.1" + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3981,9 +4037,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz", - "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz", + "integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==", "dev": true, "license": "MIT", "engines": { @@ -3998,15 +4054,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz", - "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz", + "integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/typescript-estree": "8.61.1", - "@typescript-eslint/utils": "8.61.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/utils": "8.62.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -4023,9 +4079,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz", - "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz", + "integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -4037,16 +4093,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz", - "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz", + "integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.61.1", - "@typescript-eslint/tsconfig-utils": "8.61.1", - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/visitor-keys": "8.61.1", + "@typescript-eslint/project-service": "8.62.1", + "@typescript-eslint/tsconfig-utils": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -4078,16 +4134,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz", - "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz", + "integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.61.1", - "@typescript-eslint/types": "8.61.1", - "@typescript-eslint/typescript-estree": "8.61.1" + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4102,13 +4158,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz", - "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz", + "integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/types": "8.62.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -4120,13 +4176,13 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", - "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "^1.0.0" + "@rolldown/pluginutils": "^1.0.1" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -4511,9 +4567,9 @@ "license": "MIT" }, "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==", + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz", + "integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==", "dev": true, "funding": [ { @@ -4531,8 +4587,8 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.28.2", - "caniuse-lite": "^1.0.30001787", + "browserslist": "^4.28.4", + "caniuse-lite": "^1.0.30001799", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -4558,9 +4614,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.38", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", - "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4581,9 +4637,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", "dependencies": { @@ -4594,9 +4650,9 @@ } }, "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==", + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", "dev": true, "funding": [ { @@ -4614,10 +4670,10 @@ ], "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", + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", "update-browserslist-db": "^1.2.3" }, "bin": { @@ -4644,9 +4700,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001799", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", - "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "version": "1.0.30001800", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", + "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", "dev": true, "funding": [ { @@ -4921,9 +4977,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.376", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz", - "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==", + "version": "1.5.387", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.387.tgz", + "integrity": "sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ==", "dev": true, "license": "ISC" }, @@ -4975,9 +5031,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", "dev": true, "license": "MIT" }, @@ -5047,9 +5103,9 @@ } }, "node_modules/eslint": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz", - "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", + "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", "dev": true, "license": "MIT", "workspaces": [ @@ -5267,9 +5323,9 @@ } }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5456,9 +5512,9 @@ } }, "node_modules/globals": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", - "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", "dev": true, "license": "MIT", "engines": { @@ -6275,9 +6331,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.13", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.13.tgz", - "integrity": "sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -6301,9 +6357,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.48", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", - "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", "dev": true, "license": "MIT", "engines": { @@ -6400,34 +6456,34 @@ } }, "node_modules/oxc-resolver": { - "version": "11.21.3", - "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.21.3.tgz", - "integrity": "sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.23.0.tgz", + "integrity": "sha512-f0+l598CJMOLnYPXsXxttJALH0ljtivdRMKtvHhxRuWa5FYmw5+qODARl8oYjMC/brpzKcrpdORsOBrTqhBZ9A==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxc-resolver/binding-android-arm-eabi": "11.21.3", - "@oxc-resolver/binding-android-arm64": "11.21.3", - "@oxc-resolver/binding-darwin-arm64": "11.21.3", - "@oxc-resolver/binding-darwin-x64": "11.21.3", - "@oxc-resolver/binding-freebsd-x64": "11.21.3", - "@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.3", - "@oxc-resolver/binding-linux-arm-musleabihf": "11.21.3", - "@oxc-resolver/binding-linux-arm64-gnu": "11.21.3", - "@oxc-resolver/binding-linux-arm64-musl": "11.21.3", - "@oxc-resolver/binding-linux-ppc64-gnu": "11.21.3", - "@oxc-resolver/binding-linux-riscv64-gnu": "11.21.3", - "@oxc-resolver/binding-linux-riscv64-musl": "11.21.3", - "@oxc-resolver/binding-linux-s390x-gnu": "11.21.3", - "@oxc-resolver/binding-linux-x64-gnu": "11.21.3", - "@oxc-resolver/binding-linux-x64-musl": "11.21.3", - "@oxc-resolver/binding-openharmony-arm64": "11.21.3", - "@oxc-resolver/binding-wasm32-wasi": "11.21.3", - "@oxc-resolver/binding-win32-arm64-msvc": "11.21.3", - "@oxc-resolver/binding-win32-x64-msvc": "11.21.3" + "@oxc-resolver/binding-android-arm-eabi": "11.23.0", + "@oxc-resolver/binding-android-arm64": "11.23.0", + "@oxc-resolver/binding-darwin-arm64": "11.23.0", + "@oxc-resolver/binding-darwin-x64": "11.23.0", + "@oxc-resolver/binding-freebsd-x64": "11.23.0", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.23.0", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.23.0", + "@oxc-resolver/binding-linux-arm64-gnu": "11.23.0", + "@oxc-resolver/binding-linux-arm64-musl": "11.23.0", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.23.0", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.23.0", + "@oxc-resolver/binding-linux-riscv64-musl": "11.23.0", + "@oxc-resolver/binding-linux-s390x-gnu": "11.23.0", + "@oxc-resolver/binding-linux-x64-gnu": "11.23.0", + "@oxc-resolver/binding-linux-x64-musl": "11.23.0", + "@oxc-resolver/binding-openharmony-arm64": "11.23.0", + "@oxc-resolver/binding-wasm32-wasi": "11.23.0", + "@oxc-resolver/binding-win32-arm64-msvc": "11.23.0", + "@oxc-resolver/binding-win32-x64-msvc": "11.23.0" } }, "node_modules/p-limit": { @@ -6554,9 +6610,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -6567,9 +6623,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -6745,9 +6801,9 @@ } }, "node_modules/react-router": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", - "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -6767,12 +6823,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz", - "integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==", + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", + "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", "license": "MIT", "dependencies": { - "react-router": "7.18.0" + "react-router": "7.18.1" }, "engines": { "node": ">=20.0.0" @@ -6805,9 +6861,9 @@ } }, "node_modules/recast": { - "version": "0.23.11", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", - "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "version": "0.23.12", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.12.tgz", + "integrity": "sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==", "dev": true, "license": "MIT", "dependencies": { @@ -6881,13 +6937,13 @@ } }, "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.133.0", + "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -6897,27 +6953,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" } }, "node_modules/rolldown/node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", "dev": true, "license": "MIT", "funding": { @@ -7163,9 +7219,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", - "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", + "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", "dev": true, "license": "MIT" }, @@ -7254,22 +7310,22 @@ } }, "node_modules/tldts": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.3.tgz", - "integrity": "sha512-A3BDQBeeukYPzB4QdQ1DtdlUmp4x2OCH8n5UVhEWbyANxNep8GavottKzd1xYKFJKjUgMyPT7EzOfnBO55s8Sg==", + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.6.tgz", + "integrity": "sha512-rbP0Gyx8b3Ae9yO//CU2wbSnQNoQ66m1nJdSbSHmnwKwzkkz/u8mERYU8T2rmlmy+bJvRNn84yNCW8gYqox44Q==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.4.3" + "tldts-core": "^7.4.6" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.3.tgz", - "integrity": "sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw==", + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.6.tgz", + "integrity": "sha512-TkQNGJIhlEphpHCjKodMTSe23egUZr/g+flI2qkLgiJ/maAzSgXypSLRTNH3nCmqgayEmtcJBiLcfODSAr1xoA==", "dev": true, "license": "MIT" }, @@ -7371,16 +7427,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.61.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz", - "integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz", + "integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.61.1", - "@typescript-eslint/parser": "8.61.1", - "@typescript-eslint/typescript-estree": "8.61.1", - "@typescript-eslint/utils": "8.61.1" + "@typescript-eslint/eslint-plugin": "8.62.1", + "@typescript-eslint/parser": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/utils": "8.62.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7522,16 +7578,16 @@ } }, "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", "tinyglobby": "^0.2.17" }, "bin": { @@ -7548,7 +7604,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", + "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", diff --git a/frontend/package.json b/frontend/package.json index ceb36e50..25163024 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "storybook": "storybook dev -p 6006" }, "dependencies": { + "@radix-ui/react-checkbox": "^1.3.6", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", diff --git a/frontend/src/components/ui/checkbox.stories.tsx b/frontend/src/components/ui/checkbox.stories.tsx new file mode 100644 index 00000000..d537ec87 --- /dev/null +++ b/frontend/src/components/ui/checkbox.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from "@storybook/react-vite" + +import { Checkbox } from "./checkbox" + +const meta = { + title: "Components/Checkbox", + component: Checkbox, + tags: ["autodocs"], + parameters: { layout: "centered" }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Unchecked: Story = { + args: {}, +} + +export const Checked: Story = { + args: { + checked: true, + }, +} + +export const Disabled: Story = { + args: { + disabled: true, + }, +} diff --git a/frontend/src/components/ui/checkbox.test.tsx b/frontend/src/components/ui/checkbox.test.tsx new file mode 100644 index 00000000..08e0f580 --- /dev/null +++ b/frontend/src/components/ui/checkbox.test.tsx @@ -0,0 +1,17 @@ +import { render, screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { describe, expect, it, vi } from "vitest" + +import { Checkbox } from "./checkbox" + +describe("Checkbox", () => { + it("renders and toggles checked state", async () => { + const user = userEvent.setup() + const onCheckedChange = vi.fn() + + render() + + await user.click(screen.getByRole("checkbox", { name: /accept terms/i })) + expect(onCheckedChange).toHaveBeenCalledWith(true) + }) +}) diff --git a/frontend/src/components/ui/checkbox.tsx b/frontend/src/components/ui/checkbox.tsx new file mode 100644 index 00000000..9b17e647 --- /dev/null +++ b/frontend/src/components/ui/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" + +import { Icon } from "@/components/ui/icon" +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/frontend/src/core/api/workspace-api-keys.test.ts b/frontend/src/core/api/workspace-api-keys.test.ts new file mode 100644 index 00000000..e033d7dd --- /dev/null +++ b/frontend/src/core/api/workspace-api-keys.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it, vi } from "vitest" + +vi.mock("@/core/api/client", () => ({ + apiFetch: vi.fn(), +})) + +import { apiFetch } from "@/core/api/client" + +import { + activeWorkspaceApiKeys, + fetchWorkspaceApiKeys, + type WorkspaceApiKey, +} from "./workspace-api-keys" + +const mockedApiFetch = vi.mocked(apiFetch) + +const sampleKeys: WorkspaceApiKey[] = [ + { + id: "key-1", + workspace_id: "ws-1", + client_id: "client-active", + name: "Active key", + is_active: true, + created_at: "2026-01-01T00:00:00Z", + }, + { + id: "key-2", + workspace_id: "ws-1", + client_id: "client-inactive", + name: "Inactive key", + is_active: false, + created_at: "2026-01-02T00:00:00Z", + }, +] + +describe("activeWorkspaceApiKeys", () => { + it("filters out inactive keys", () => { + const result = activeWorkspaceApiKeys(sampleKeys) + + expect(result).toHaveLength(1) + expect(result[0]?.name).toBe("Active key") + expect(result.every((key) => key.is_active)).toBe(true) + }) +}) + +describe("fetchWorkspaceApiKeys", () => { + it("returns keys from the API", async () => { + mockedApiFetch.mockResolvedValue({ + ok: true, + json: async () => sampleKeys, + } as Response) + + const result = await fetchWorkspaceApiKeys("ws-1") + + expect(mockedApiFetch).toHaveBeenCalledWith("/api/v1/workspaces/ws-1/api-keys") + expect(result).toEqual(sampleKeys) + }) + + it("throws when the API response is not ok", async () => { + mockedApiFetch.mockResolvedValue({ ok: false } as Response) + + await expect(fetchWorkspaceApiKeys("ws-1")).rejects.toThrow("Failed to load API keys") + }) +}) diff --git a/frontend/src/core/api/workspace-api-keys.ts b/frontend/src/core/api/workspace-api-keys.ts new file mode 100644 index 00000000..408a33f2 --- /dev/null +++ b/frontend/src/core/api/workspace-api-keys.ts @@ -0,0 +1,24 @@ +import { apiFetch } from "@/core/api/client" + +export interface WorkspaceApiKey { + id: string + workspace_id: string + client_id: string + name: string + is_active: boolean + last_used_at?: string | null + created_at: string + expires_at?: string | null +} + +export async function fetchWorkspaceApiKeys(workspaceId: string): Promise { + const res = await apiFetch(`/api/v1/workspaces/${workspaceId}/api-keys`) + if (!res.ok) { + throw new Error("Failed to load API keys") + } + return (await res.json()) as WorkspaceApiKey[] +} + +export function activeWorkspaceApiKeys(keys: WorkspaceApiKey[]): WorkspaceApiKey[] { + return keys.filter((key) => key.is_active) +} diff --git a/frontend/src/core/router/routes.ts b/frontend/src/core/router/routes.ts index 1fcea2a5..e03d63a6 100644 --- a/frontend/src/core/router/routes.ts +++ b/frontend/src/core/router/routes.ts @@ -7,6 +7,8 @@ export const ROUTES = { pattern: "/workspaces/:wid", overview: (workspaceId: string) => `/workspaces/${workspaceId}/overview`, email: (workspaceId: string) => `/workspaces/${workspaceId}/email`, + emailWithMessage: (workspaceId: string, messageId: string) => + `/workspaces/${workspaceId}/email?message=${encodeURIComponent(messageId)}`, emailTemplates: (workspaceId: string) => `/workspaces/${workspaceId}/email/templates`, sms: (workspaceId: string) => `/workspaces/${workspaceId}/sms`, push: (workspaceId: string) => `/workspaces/${workspaceId}/push`, diff --git a/frontend/src/features/auth/pages/login.page.test.tsx b/frontend/src/features/auth/pages/login.page.test.tsx new file mode 100644 index 00000000..93725813 --- /dev/null +++ b/frontend/src/features/auth/pages/login.page.test.tsx @@ -0,0 +1,44 @@ +import { render, screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { MemoryRouter } from "react-router-dom" +import { describe, expect, it, vi } from "vitest" + +import { signInWithPassword } from "../auth.api" +import { LoginPage } from "./login.page" + +vi.mock("../auth.api", () => ({ + signInWithPassword: vi.fn(), +})) + +const mockedSignIn = vi.mocked(signInWithPassword) + +describe("LoginPage", () => { + it("renders sign-in form", () => { + render( + + + , + ) + + expect(screen.getByRole("heading", { name: /sign in/i })).toBeInTheDocument() + expect(screen.getByLabelText(/email address/i)).toBeInTheDocument() + expect(screen.getByLabelText(/^password$/i)).toBeInTheDocument() + }) + + it("shows API error on failed login", async () => { + mockedSignIn.mockResolvedValue({ ok: false, message: "Invalid credentials" }) + + render( + + + , + ) + + const user = userEvent.setup() + await user.type(screen.getByLabelText(/email address/i), "user@example.com") + await user.type(screen.getByLabelText(/^password$/i), "wrong") + await user.click(screen.getByRole("button", { name: /^login$/i })) + + expect(await screen.findByText("Invalid credentials")).toBeInTheDocument() + }) +}) diff --git a/frontend/src/features/auth/pages/register.page.test.tsx b/frontend/src/features/auth/pages/register.page.test.tsx new file mode 100644 index 00000000..24a41ee8 --- /dev/null +++ b/frontend/src/features/auth/pages/register.page.test.tsx @@ -0,0 +1,45 @@ +import { render, screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { MemoryRouter } from "react-router-dom" +import { describe, expect, it, vi } from "vitest" + +import { registerAccount } from "../auth.api" +import { RegisterPage } from "./register.page" + +vi.mock("../auth.api", () => ({ + registerAccount: vi.fn(), +})) + +const mockedRegister = vi.mocked(registerAccount) + +describe("RegisterPage", () => { + it("renders sign-up form", () => { + render( + + + , + ) + + expect(screen.getByRole("heading", { name: /sign up/i })).toBeInTheDocument() + expect(screen.getByLabelText(/full name/i)).toBeInTheDocument() + expect(screen.getByLabelText(/email address/i)).toBeInTheDocument() + }) + + it("shows API error on failed registration", async () => { + mockedRegister.mockResolvedValue({ ok: false, message: "Email already taken" }) + + render( + + + , + ) + + const user = userEvent.setup() + await user.type(screen.getByLabelText(/full name/i), "Demo User") + await user.type(screen.getByLabelText(/email address/i), "user@example.com") + await user.type(screen.getByLabelText(/^password$/i), "secret123456") + await user.click(screen.getByRole("button", { name: /sign up/i })) + + expect(await screen.findByText("Email already taken")).toBeInTheDocument() + }) +}) diff --git a/frontend/src/features/inbox/components/email-content/email-content.tsx b/frontend/src/features/inbox/components/email-content/email-content.tsx index d36b9c97..cd91dc97 100644 --- a/frontend/src/features/inbox/components/email-content/email-content.tsx +++ b/frontend/src/features/inbox/components/email-content/email-content.tsx @@ -27,8 +27,8 @@ function formatFullTime(dateStr: string) { export function EmailContent({ message, onDelete, isDeleting = false }: EmailContentProps) { const sender = message.email.from_name ? `${message.email.from_name} <${message.email.from}>` - : message.email.from || "Unknown Sender" - + : message.email.from || "Unknown sender" + const recipients = (message.email.to || []).join(", ") const formattedDate = formatFullTime(message.created_at) @@ -48,7 +48,7 @@ export function EmailContent({ message, onDelete, isDeleting = false }: EmailCon {message.email.subject || "(No Subject)"}

- {sender} • {formattedDate} + To: {recipients || "—"} • {formattedDate}

diff --git a/frontend/src/features/inbox/components/email-item/email-item.test.tsx b/frontend/src/features/inbox/components/email-item/email-item.test.tsx index a204fab3..17c857d8 100644 --- a/frontend/src/features/inbox/components/email-item/email-item.test.tsx +++ b/frontend/src/features/inbox/components/email-item/email-item.test.tsx @@ -21,7 +21,7 @@ const mockEmail = { describe("EmailItem Component", () => { it("renders correctly with email properties", () => { render() - expect(screen.getByText("John Doe")).toBeInTheDocument() + expect(screen.getByText("recipient@example.com")).toBeInTheDocument() expect(screen.getByText("Test Subject")).toBeInTheDocument() expect(screen.getByText("This is the body snippet of the test email.")).toBeInTheDocument() }) diff --git a/frontend/src/features/inbox/components/email-item/email-item.tsx b/frontend/src/features/inbox/components/email-item/email-item.tsx index c3746504..5e995429 100644 --- a/frontend/src/features/inbox/components/email-item/email-item.tsx +++ b/frontend/src/features/inbox/components/email-item/email-item.tsx @@ -6,10 +6,15 @@ interface EmailItemProps { onClick: () => void } -function getSenderInitial(nameOrEmail: string) { - if (!nameOrEmail) return "?" - const cleaned = nameOrEmail.trim() - return cleaned[0].toUpperCase() +function formatRecipients(to: string[] | undefined) { + if (!to || to.length === 0) return "Unknown recipient" + return to.join(", ") +} + +function getRecipientInitial(recipient: string) { + if (!recipient) return "?" + const cleaned = recipient.trim() + return cleaned[0]?.toUpperCase() ?? "?" } function formatTime(dateStr: string) { @@ -26,8 +31,8 @@ function formatTime(dateStr: string) { } export function EmailItem({ message, isSelected, onClick }: EmailItemProps) { - const senderName = message.email.from_name || message.email.from || "Unknown Sender" - const initial = getSenderInitial(senderName) + const recipientLabel = formatRecipients(message.email.to) + const initial = getRecipientInitial(message.email.to?.[0] ?? recipientLabel) const timestamp = formatTime(message.created_at) const snippet = message.email.plain_text || message.email.html || message.email.subject || "" @@ -57,7 +62,7 @@ export function EmailItem({ message, isSelected, onClick }: EmailItemProps) { {initial} - {senderName} + {recipientLabel} diff --git a/frontend/src/features/inbox/components/email-list/email-list.test.tsx b/frontend/src/features/inbox/components/email-list/email-list.test.tsx index 9b94f21d..29605e05 100644 --- a/frontend/src/features/inbox/components/email-list/email-list.test.tsx +++ b/frontend/src/features/inbox/components/email-list/email-list.test.tsx @@ -26,6 +26,6 @@ describe("EmailList Component", () => { expect(screen.getByText("Inbox")).toBeInTheDocument() expect(screen.getByText("1")).toBeInTheDocument() expect(screen.getByPlaceholderText("Search inbox...")).toBeInTheDocument() - expect(screen.getByText("John Doe")).toBeInTheDocument() + expect(screen.getByText("recipient@example.com")).toBeInTheDocument() }) }) diff --git a/frontend/src/features/inbox/components/email-list/email-list.tsx b/frontend/src/features/inbox/components/email-list/email-list.tsx index ac3beef4..ab5f297b 100644 --- a/frontend/src/features/inbox/components/email-list/email-list.tsx +++ b/frontend/src/features/inbox/components/email-list/email-list.tsx @@ -1,5 +1,6 @@ import { useState } from "react" import type { StoredEmail } from "../../inbox.types" +import { Button } from "@/components/ui/button" import { SearchInput } from "@/components/ui/search-input" import { EmailItem } from "../email-item" @@ -7,16 +8,27 @@ interface EmailListProps { messages: StoredEmail[] selectedMessageId: string | null onSelectMessage: (messageId: string) => void + hasMore?: boolean + isLoadingMore?: boolean + onLoadMore?: () => void } -export function EmailList({ messages, selectedMessageId, onSelectMessage }: EmailListProps) { +export function EmailList({ + messages, + selectedMessageId, + onSelectMessage, + hasMore = false, + isLoadingMore = false, + onLoadMore, +}: EmailListProps) { const [searchQuery, setSearchQuery] = useState("") const filtered = messages.filter((msg) => { const q = searchQuery.toLowerCase() const subject = (msg.email.subject || "").toLowerCase() + const recipients = (msg.email.to || []).join(" ").toLowerCase() const sender = (msg.email.from_name || msg.email.from || "").toLowerCase() - return subject.includes(q) || sender.includes(q) + return subject.includes(q) || recipients.includes(q) || sender.includes(q) }) return ( @@ -51,6 +63,21 @@ export function EmailList({ messages, selectedMessageId, onSelectMessage }: Emai {searchQuery ? "No messages match your search." : "Your inbox is empty."} )} + + {hasMore && !searchQuery && onLoadMore && ( +
+ +
+ )} ) diff --git a/frontend/src/features/inbox/components/get-started-dialog/get-started-dialog.stories.tsx b/frontend/src/features/inbox/components/get-started-dialog/get-started-dialog.stories.tsx new file mode 100644 index 00000000..7eb2f39f --- /dev/null +++ b/frontend/src/features/inbox/components/get-started-dialog/get-started-dialog.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from "@storybook/react-vite" +import { MemoryRouter } from "react-router-dom" + +import { GetStartedDialog } from "./get-started-dialog" + +const meta = { + title: "Features/Inbox/GetStartedDialog", + component: GetStartedDialog, + decorators: [ + (Story) => ( + + + + ), + ], +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Open: Story = { + args: { + open: true, + onOpenChange: () => undefined, + workspaceId: "00000000-0000-0000-0000-000000000001", + }, +} diff --git a/frontend/src/features/inbox/components/get-started-dialog/get-started-dialog.test.tsx b/frontend/src/features/inbox/components/get-started-dialog/get-started-dialog.test.tsx new file mode 100644 index 00000000..dde552c4 --- /dev/null +++ b/frontend/src/features/inbox/components/get-started-dialog/get-started-dialog.test.tsx @@ -0,0 +1,52 @@ +import { render, screen } from "@testing-library/react" +import { describe, expect, it, vi } from "vitest" +import { MemoryRouter } from "react-router-dom" + +import { GetStartedDialog } from "./get-started-dialog" +import { useGetStartedContext } from "../../hooks/use-get-started-context.hook" + +vi.mock("../../hooks/use-get-started-context.hook", () => ({ + useGetStartedContext: vi.fn(), +})) + +const mockedUseGetStartedContext = vi.mocked(useGetStartedContext) + +describe("GetStartedDialog", () => { + it("renders workspace credentials and curl examples", () => { + mockedUseGetStartedContext.mockReturnValue({ + context: { + workspaceId: "00000000-0000-0000-0000-000000000001", + apiKeys: [{ client_id: "demo-client-id", name: "Demo", is_active: true }], + }, + isLoading: false, + error: null, + }) + + render( + + + , + ) + + expect(screen.getByText("Your credentials")).toBeInTheDocument() + expect(screen.getByText("demo-client-id")).toBeInTheDocument() + expect(screen.getAllByText(/X-Workspace-Key: 00000000-0000-0000-0000-000000000001/).length).toBeGreaterThan(0) + expect(screen.getByText(/\/v1\/email/)).toBeInTheDocument() + }) + + it("shows loading state", () => { + mockedUseGetStartedContext.mockReturnValue({ + context: null, + isLoading: true, + error: null, + }) + + render( + + + , + ) + + expect(screen.getByText(/loading workspace credentials/i)).toBeInTheDocument() + }) +}) diff --git a/frontend/src/features/inbox/components/get-started-dialog/get-started-dialog.tsx b/frontend/src/features/inbox/components/get-started-dialog/get-started-dialog.tsx new file mode 100644 index 00000000..49f3ff72 --- /dev/null +++ b/frontend/src/features/inbox/components/get-started-dialog/get-started-dialog.tsx @@ -0,0 +1,134 @@ +import { Link } from "react-router-dom" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Spinner } from "@/components/ui/spinner" +import { ROUTES } from "@/core/router/routes" +import { buildGatewayCurlExamples } from "../../get-started-dialog.utils" +import { useGetStartedContext } from "../../hooks/use-get-started-context.hook" + +interface GetStartedDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + workspaceId?: string +} + +function developerSettingsPath(workspaceId: string) { + return `${ROUTES.workspace.settings(workspaceId)}?tab=developer` +} + +export function GetStartedDialog({ open, onOpenChange, workspaceId }: GetStartedDialogProps) { + const { context, isLoading, error } = useGetStartedContext(workspaceId, open) + const developerPath = workspaceId ? developerSettingsPath(workspaceId) : ROUTES.workspaces + + const primaryKey = context?.apiKeys[0] + const examples = + context && primaryKey + ? buildGatewayCurlExamples(context.workspaceId, primaryKey.client_id) + : null + + return ( + + + + Get started + + Send test messages with your workspace API key and ID. + + +
+ {isLoading ? ( +
+ + Loading workspace credentials… +
+ ) : null} + + {error ? ( +

+ {error} +

+ ) : null} + + {context ? ( +
+

+ Your credentials +

+
+
+
Workspace ID
+
{context.workspaceId}
+
+
+
Header
+
+ X-Workspace-Key: {context.workspaceId} +
+
+ {primaryKey ? ( +
+
Client ID
+
{primaryKey.client_id}
+
+ ) : ( +

+ No API keys yet. Create one in{" "} + + Settings → Developer + + . +

+ )} +
+
Client secret
+
+ Shown once when you generate or regenerate a key in{" "} + + Settings → Developer + + . Replace YOUR_CLIENT_SECRET in the + examples below. +
+
+
+
+ ) : null} + + {examples ? ( +
+
+

Email

+
+                  {examples.email}
+                
+
+
+

SMS

+
+                  {examples.sms}
+                
+
+
+ ) : null} + + {!isLoading && !context && !error ? ( +

Open this dialog from a workspace to load credentials.

+ ) : null} +
+ + + +
+
+ ) +} diff --git a/frontend/src/features/inbox/components/get-started-dialog/index.ts b/frontend/src/features/inbox/components/get-started-dialog/index.ts new file mode 100644 index 00000000..989854b9 --- /dev/null +++ b/frontend/src/features/inbox/components/get-started-dialog/index.ts @@ -0,0 +1 @@ +export { GetStartedDialog } from "./get-started-dialog" diff --git a/frontend/src/features/inbox/components/send-test-modal/index.ts b/frontend/src/features/inbox/components/send-test-modal/index.ts deleted file mode 100644 index cb31fba5..00000000 --- a/frontend/src/features/inbox/components/send-test-modal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./send-test-modal" diff --git a/frontend/src/features/inbox/components/send-test-modal/send-test-modal.stories.tsx b/frontend/src/features/inbox/components/send-test-modal/send-test-modal.stories.tsx deleted file mode 100644 index bdee6290..00000000 --- a/frontend/src/features/inbox/components/send-test-modal/send-test-modal.stories.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite" -import { useState } from "react" - -import { Button } from "@/components/ui/button" -import { SendTestModal } from "./send-test-modal" - -const meta = { - title: "Features/Inbox/SendTestModal", - component: SendTestModal, - tags: ["autodocs"], - parameters: { layout: "centered" }, -} satisfies Meta - -export default meta - -type Story = StoryObj - -function SendTestModalDemo() { - const [open, setOpen] = useState(true) - return ( - <> - - undefined} - initialChannel="email" - /> - - ) -} - -export const EmailChannel: Story = { - render: () => , - args: { - workspaceId: "00000000-0000-0000-0000-000000000001", - open: true, - onOpenChange: () => undefined, - onSent: () => undefined, - initialChannel: "email", - }, -} diff --git a/frontend/src/features/inbox/components/send-test-modal/send-test-modal.test.tsx b/frontend/src/features/inbox/components/send-test-modal/send-test-modal.test.tsx deleted file mode 100644 index b563a3bf..00000000 --- a/frontend/src/features/inbox/components/send-test-modal/send-test-modal.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { render, screen } from "@testing-library/react" -import { describe, expect, it, vi } from "vitest" - -import { SendTestModal } from "./send-test-modal" - -vi.mock("../../inbox.api", () => ({ - sendTestRequest: vi.fn(), -})) - -describe("SendTestModal Component", () => { - it("renders correctly when open", () => { - render( - - ) - expect(screen.getByText("Send Test Request")).toBeInTheDocument() - expect(screen.getByLabelText("To (comma-separated)")).toBeInTheDocument() - }) -}) diff --git a/frontend/src/features/inbox/components/send-test-modal/send-test-modal.tsx b/frontend/src/features/inbox/components/send-test-modal/send-test-modal.tsx deleted file mode 100644 index 5491f047..00000000 --- a/frontend/src/features/inbox/components/send-test-modal/send-test-modal.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import { useState } from "react" -import { Icon } from "@/components/ui/icon" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import { sendTestRequest } from "../../inbox.api" -import type { MessageChannel } from "../../inbox.types" - -interface SendTestModalProps { - workspaceId: string - open: boolean - onOpenChange: (open: boolean) => void - onSent: () => void - initialChannel?: MessageChannel -} - -export function SendTestModal({ - workspaceId, - open, - onOpenChange, - onSent, - initialChannel = "email", -}: SendTestModalProps) { - const [channel, setChannel] = useState(initialChannel) - const [to, setTo] = useState("") - const [subject, setSubject] = useState("") - const [body, setBody] = useState("") - const [html, setHtml] = useState("") - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(null) - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setIsLoading(true) - setError(null) - - let payload: Record = {} - if (channel === "email") { - payload = { - to: to.split(",").map((s) => s.trim()), - subject, - html, - } - } else if (channel === "sms") { - payload = { to, body } - } else if (channel === "push") { - payload = { to, title: subject, body } - } else if (channel === "chat") { - payload = { to, body } - } - - const res = await sendTestRequest(workspaceId, channel, payload) - setIsLoading(false) - - if (!res.ok) { - setError(res.message ?? "Failed to trigger test request") - } else { - onSent() - onOpenChange(false) - } - } - - const handleChannelChange = (newChannel: "email" | "sms" | "push" | "chat") => { - setChannel(newChannel) - setError(null) - if (newChannel === "email") { - setTo("user@example.com") - setSubject("Welcome to Message Gateway!") - } else if (newChannel === "sms") { - setTo("+15551234567") - setBody("This is a test SMS message.") - } else if (newChannel === "push") { - setTo("device-token-12345") - setSubject("New Notification") - setBody("This is a test push notification.") - } else if (newChannel === "chat") { - setTo("slack-channel-id") - setBody("Hello from the Chat API!") - } - } - - return ( - - - - Send Test Request - Trigger a gateway send for your workspace. - - -
-
- {error && ( -
- - {error} -
- )} - -
- Channel -
- {(["email", "sms", "push", "chat"] as const).map((ch) => ( - - ))} -
-
- -
- - setTo(e.target.value)} - className="bg-background text-sm" - /> -
- - {(channel === "email" || channel === "push") && ( -
- - setSubject(e.target.value)} - className="bg-background text-sm" - /> -
- )} - - {channel === "email" && ( -
- -