diff --git a/.golangci.yml b/.golangci.yml
index 782a3da9..04ba18ca 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,15 +1,18 @@
version: "2"
+
linters:
disable:
- errcheck
+
settings:
staticcheck:
checks:
- all
- - -QF1003
- - -QF1008
- - -S1017
- - -SA9003
+ - -SA9003 # Empty branch - sometimes used for clarity
+ - -QF1003 # Tagged switch suggestion - keep explicit for readability
+ - -QF1008 # Remove embedded field from selector - keep explicit for clarity
+ - -S1017 # TrimPrefix suggestion - keep explicit for readability
+
exclusions:
generated: lax
presets:
@@ -21,9 +24,11 @@ linters:
- third_party$
- builtin$
- examples$
+
issues:
max-issues-per-linter: 0
max-same-issues: 0
+
formatters:
exclusions:
generated: lax
diff --git a/Makefile b/Makefile
index f56a9651..afb6ac96 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: build build-no-restart install clean test deploy
+.PHONY: build build-no-restart install clean test deploy web
# Configuration
SERVER ?= root@cloud-claude
@@ -21,6 +21,24 @@ build-ty:
build-taskd:
$(GO) build -o bin/taskd ./cmd/taskd
+build-taskweb:
+ go build -o bin/taskweb ./cmd/taskweb
+
+# Build web frontend
+web:
+ cd web && npm install && npm run build
+
+# Build taskweb with embedded frontend
+build-taskweb-full: web build-taskweb
+
+# Run web UI in development mode (connects to local database)
+# Usage: make webdev (run API server), then in another terminal: make webui
+webdev:
+ go run ./cmd/taskweb-dev
+
+webui:
+ cd web && npm install && npm run dev
+
# Restart daemon if it's running (silent if not). Never fail the build if we lack permissions.
restart-daemon:
@if pgrep -f "ty daemon" > /dev/null; then \
diff --git a/cmd/taskweb-dev/main.go b/cmd/taskweb-dev/main.go
new file mode 100644
index 00000000..87baec84
--- /dev/null
+++ b/cmd/taskweb-dev/main.go
@@ -0,0 +1,60 @@
+// taskweb-dev runs the web API server locally for development.
+// It connects to your local task database and serves the API on port 8081.
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "github.com/bborn/workflow/internal/db"
+ "github.com/bborn/workflow/internal/webapi"
+)
+
+func main() {
+ // Use the same database as the local task CLI
+ dbPath := db.DefaultPath()
+ fmt.Printf("Using database: %s\n", dbPath)
+
+ database, err := db.Open(dbPath)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to open database: %v\n", err)
+ os.Exit(1)
+ }
+ defer database.Close()
+
+ // Create the API server with dev mode enabled
+ server := webapi.New(webapi.Config{
+ Addr: ":8081",
+ DB: database,
+ DevMode: true,
+ DevOrigin: "http://localhost:5173",
+ })
+
+ // Handle graceful shutdown
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
+
+ go func() {
+ <-sigCh
+ fmt.Println("\nShutting down...")
+ cancel()
+ }()
+
+ fmt.Println("Starting web API server on http://localhost:8081")
+ fmt.Println("Frontend should run on http://localhost:5173")
+ fmt.Println()
+ fmt.Println("To start the frontend:")
+ fmt.Println(" cd web && npm install && npm run dev")
+ fmt.Println()
+
+ if err := server.Start(ctx); err != nil {
+ fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
+ os.Exit(1)
+ }
+}
diff --git a/cmd/taskweb/main.go b/cmd/taskweb/main.go
new file mode 100644
index 00000000..510c18c6
--- /dev/null
+++ b/cmd/taskweb/main.go
@@ -0,0 +1,124 @@
+// Package main provides the entry point for the taskweb server.
+// This server provides a web-based UI for taskyou with Fly Sprites integration.
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "github.com/bborn/workflow/internal/hostdb"
+ "github.com/bborn/workflow/internal/webserver"
+ "github.com/charmbracelet/log"
+ "github.com/spf13/cobra"
+)
+
+var (
+ addr string
+ dbPath string
+ baseURL string
+ secure bool
+ domain string
+)
+
+func main() {
+ rootCmd := &cobra.Command{
+ Use: "taskweb",
+ Short: "Web server for taskyou with Fly Sprites integration",
+ Long: `taskweb provides a web-based UI for taskyou.
+
+Each user gets their own isolated Fly Sprite running the task executor.
+User data (tasks, projects) stays entirely within their sprite.
+This host service handles only authentication and sprite orchestration.
+
+Environment variables:
+ TASKWEB_DATABASE_PATH - Path to host database (default: ~/.local/share/taskweb/taskweb.db)
+ GOOGLE_CLIENT_ID - Google OAuth client ID
+ GOOGLE_CLIENT_SECRET - Google OAuth client secret
+ GITHUB_CLIENT_ID - GitHub OAuth client ID
+ GITHUB_CLIENT_SECRET - GitHub OAuth client secret
+ SPRITES_TOKEN - Fly Sprites API token`,
+ Run: runServer,
+ }
+
+ rootCmd.Flags().StringVarP(&addr, "addr", "a", getEnvOrDefault("TASKWEB_ADDR", ":8080"), "Server address")
+ rootCmd.Flags().StringVarP(&dbPath, "db", "d", hostdb.DefaultPath(), "Database path")
+ rootCmd.Flags().StringVar(&baseURL, "base-url", getEnvOrDefault("TASKWEB_BASE_URL", "http://localhost:8080"), "Base URL for OAuth callbacks")
+ rootCmd.Flags().BoolVar(&secure, "secure", getEnvOrDefault("TASKWEB_SECURE", "") == "true", "Use secure cookies (HTTPS)")
+ rootCmd.Flags().StringVar(&domain, "domain", getEnvOrDefault("TASKWEB_DOMAIN", ""), "Cookie domain")
+
+ if err := rootCmd.Execute(); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
+
+func runServer(cmd *cobra.Command, args []string) {
+ logger := log.NewWithOptions(os.Stderr, log.Options{
+ Prefix: "taskweb",
+ })
+
+ // Open host database
+ db, err := hostdb.Open(dbPath)
+ if err != nil {
+ logger.Fatal("failed to open database", "error", err)
+ }
+ defer db.Close()
+
+ logger.Info("database opened", "path", dbPath)
+
+ // Create server
+ server, err := webserver.New(webserver.Config{
+ Addr: addr,
+ DB: db,
+ BaseURL: baseURL,
+ Secure: secure,
+ Domain: domain,
+ })
+ if err != nil {
+ logger.Fatal("failed to create server", "error", err)
+ }
+
+ // Setup context with signal handling
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+
+ go func() {
+ sig := <-sigCh
+ logger.Info("received signal, shutting down", "signal", sig)
+ cancel()
+ }()
+
+ // Log configuration
+ logger.Info("starting server",
+ "addr", addr,
+ "base_url", baseURL,
+ "secure", secure,
+ )
+
+ // Check OAuth configuration
+ if os.Getenv("GOOGLE_CLIENT_ID") == "" && os.Getenv("GITHUB_CLIENT_ID") == "" {
+ logger.Warn("no OAuth providers configured - authentication will not work")
+ }
+
+ if os.Getenv("SPRITES_TOKEN") == "" {
+ logger.Warn("SPRITES_TOKEN not set - sprite management will not work")
+ }
+
+ // Start server
+ if err := server.Start(ctx); err != nil {
+ logger.Fatal("server error", "error", err)
+ }
+}
+
+func getEnvOrDefault(key, defaultValue string) string {
+ if v := os.Getenv(key); v != "" {
+ return v
+ }
+ return defaultValue
+}
diff --git a/go.mod b/go.mod
index 732a7d25..e3ee5fd0 100644
--- a/go.mod
+++ b/go.mod
@@ -11,13 +11,21 @@ require (
github.com/charmbracelet/log v0.4.1
github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309
github.com/charmbracelet/wish v1.4.7
+ github.com/fsnotify/fsnotify v1.9.0
+ github.com/google/uuid v1.6.0
+ github.com/gorilla/websocket v1.5.3
github.com/spf13/cobra v1.10.2
+ github.com/superfly/sprites-go v0.0.0-20260112234611-135551a277ef
golang.org/x/crypto v0.37.0
+ golang.org/x/oauth2 v0.34.0
golang.org/x/term v0.31.0
+ gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.42.2
)
require (
+ cloud.google.com/go/compute/metadata v0.3.0 // indirect
+ github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/atotto/clipboard v0.1.4 // indirect
@@ -40,9 +48,7 @@ require (
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
- github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
- github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
@@ -58,7 +64,6 @@ require (
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
- github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.8 // indirect
@@ -67,8 +72,8 @@ require (
golang.org/x/net v0.36.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.24.0 // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
+ nhooyr.io/websocket v1.8.17 // indirect
)
diff --git a/go.sum b/go.sum
index 56d0eb24..284d4793 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,9 @@
+cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
+cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
+github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
+github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
@@ -83,12 +87,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
-github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -121,14 +125,14 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
-github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/superfly/sprites-go v0.0.0-20260112234611-135551a277ef h1:n5iTBxDcVWMq43jqeLUaG0i2xJsdOJIs8WTXOtz3NlY=
+github.com/superfly/sprites-go v0.0.0-20260112234611-135551a277ef/go.mod h1:4zltGIGJa3HV+XumRyNn4BmhlavbUZH3Uh5xJNaDwsY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
@@ -145,6 +149,8 @@ golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
+golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
+golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -157,6 +163,7 @@ golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -186,3 +193,5 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
+nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
diff --git a/internal/auth/oauth.go b/internal/auth/oauth.go
new file mode 100644
index 00000000..41e0df79
--- /dev/null
+++ b/internal/auth/oauth.go
@@ -0,0 +1,169 @@
+package auth
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+
+ "golang.org/x/oauth2"
+)
+
+// UserInfo represents user information from OAuth providers.
+type UserInfo struct {
+ Provider Provider
+ ProviderAccountID string
+ Email string
+ Name string
+ AvatarURL string
+}
+
+// ExchangeCode exchanges an authorization code for tokens.
+func ExchangeCode(ctx context.Context, config *Config, code string) (*oauth2.Token, error) {
+ token, err := config.OAuth2.Exchange(ctx, code)
+ if err != nil {
+ return nil, fmt.Errorf("exchange code: %w", err)
+ }
+ return token, nil
+}
+
+// GetUserInfo fetches user information from the OAuth provider.
+func GetUserInfo(ctx context.Context, config *Config, token *oauth2.Token) (*UserInfo, error) {
+ switch config.Provider {
+ case ProviderGoogle:
+ return getGoogleUserInfo(ctx, config, token)
+ case ProviderGitHub:
+ return getGitHubUserInfo(ctx, config, token)
+ default:
+ return nil, fmt.Errorf("unsupported provider: %s", config.Provider)
+ }
+}
+
+// getGoogleUserInfo fetches user info from Google.
+func getGoogleUserInfo(ctx context.Context, config *Config, token *oauth2.Token) (*UserInfo, error) {
+ client := config.OAuth2.Client(ctx, token)
+
+ resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
+ if err != nil {
+ return nil, fmt.Errorf("get user info: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("get user info failed: %s - %s", resp.Status, string(body))
+ }
+
+ var data struct {
+ ID string `json:"id"`
+ Email string `json:"email"`
+ VerifiedEmail bool `json:"verified_email"`
+ Name string `json:"name"`
+ Picture string `json:"picture"`
+ }
+
+ if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
+ return nil, fmt.Errorf("decode response: %w", err)
+ }
+
+ return &UserInfo{
+ Provider: ProviderGoogle,
+ ProviderAccountID: data.ID,
+ Email: data.Email,
+ Name: data.Name,
+ AvatarURL: data.Picture,
+ }, nil
+}
+
+// getGitHubUserInfo fetches user info from GitHub.
+func getGitHubUserInfo(ctx context.Context, config *Config, token *oauth2.Token) (*UserInfo, error) {
+ client := config.OAuth2.Client(ctx, token)
+
+ // Get user profile
+ resp, err := client.Get("https://api.github.com/user")
+ if err != nil {
+ return nil, fmt.Errorf("get user info: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("get user info failed: %s - %s", resp.Status, string(body))
+ }
+
+ var userData struct {
+ ID int64 `json:"id"`
+ Login string `json:"login"`
+ Name string `json:"name"`
+ Email string `json:"email"`
+ AvatarURL string `json:"avatar_url"`
+ }
+
+ if err := json.NewDecoder(resp.Body).Decode(&userData); err != nil {
+ return nil, fmt.Errorf("decode user response: %w", err)
+ }
+
+ email := userData.Email
+ name := userData.Name
+ if name == "" {
+ name = userData.Login
+ }
+
+ // If email is not in profile, fetch from emails API
+ if email == "" {
+ emailResp, err := client.Get("https://api.github.com/user/emails")
+ if err != nil {
+ return nil, fmt.Errorf("get user emails: %w", err)
+ }
+ defer emailResp.Body.Close()
+
+ if emailResp.StatusCode == http.StatusOK {
+ var emails []struct {
+ Email string `json:"email"`
+ Primary bool `json:"primary"`
+ Verified bool `json:"verified"`
+ }
+
+ if err := json.NewDecoder(emailResp.Body).Decode(&emails); err == nil {
+ // Find primary verified email
+ for _, e := range emails {
+ if e.Primary && e.Verified {
+ email = e.Email
+ break
+ }
+ }
+ // Fallback to any verified email
+ if email == "" {
+ for _, e := range emails {
+ if e.Verified {
+ email = e.Email
+ break
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if email == "" {
+ return nil, fmt.Errorf("could not get email from GitHub")
+ }
+
+ return &UserInfo{
+ Provider: ProviderGitHub,
+ ProviderAccountID: fmt.Sprintf("%d", userData.ID),
+ Email: email,
+ Name: name,
+ AvatarURL: userData.AvatarURL,
+ }, nil
+}
+
+// TokenExpiry returns the token expiry time, or nil if unknown.
+func TokenExpiry(token *oauth2.Token) *time.Time {
+ if token.Expiry.IsZero() {
+ return nil
+ }
+ return &token.Expiry
+}
diff --git a/internal/auth/providers.go b/internal/auth/providers.go
new file mode 100644
index 00000000..42956a67
--- /dev/null
+++ b/internal/auth/providers.go
@@ -0,0 +1,82 @@
+// Package auth provides OAuth2 authentication for the web UI.
+package auth
+
+import (
+ "os"
+
+ "golang.org/x/oauth2"
+ "golang.org/x/oauth2/github"
+ "golang.org/x/oauth2/google"
+)
+
+// Provider represents an OAuth2 provider.
+type Provider string
+
+const (
+ ProviderGoogle Provider = "google"
+ ProviderGitHub Provider = "github"
+)
+
+// Config holds OAuth configuration for a provider.
+type Config struct {
+ OAuth2 *oauth2.Config
+ Provider Provider
+}
+
+// GoogleConfig returns the OAuth2 config for Google.
+func GoogleConfig(redirectURL string) *Config {
+ return &Config{
+ Provider: ProviderGoogle,
+ OAuth2: &oauth2.Config{
+ ClientID: os.Getenv("GOOGLE_CLIENT_ID"),
+ ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
+ RedirectURL: redirectURL,
+ Scopes: []string{
+ "https://www.googleapis.com/auth/userinfo.email",
+ "https://www.googleapis.com/auth/userinfo.profile",
+ },
+ Endpoint: google.Endpoint,
+ },
+ }
+}
+
+// GitHubConfig returns the OAuth2 config for GitHub.
+func GitHubConfig(redirectURL string) *Config {
+ return &Config{
+ Provider: ProviderGitHub,
+ OAuth2: &oauth2.Config{
+ ClientID: os.Getenv("GITHUB_CLIENT_ID"),
+ ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
+ RedirectURL: redirectURL,
+ Scopes: []string{
+ "user:email",
+ "read:user",
+ },
+ Endpoint: github.Endpoint,
+ },
+ }
+}
+
+// GetConfig returns the OAuth2 config for the specified provider.
+func GetConfig(provider Provider, redirectURL string) *Config {
+ switch provider {
+ case ProviderGoogle:
+ return GoogleConfig(redirectURL)
+ case ProviderGitHub:
+ return GitHubConfig(redirectURL)
+ default:
+ return nil
+ }
+}
+
+// IsConfigured checks if a provider has valid credentials configured.
+func IsConfigured(provider Provider) bool {
+ switch provider {
+ case ProviderGoogle:
+ return os.Getenv("GOOGLE_CLIENT_ID") != "" && os.Getenv("GOOGLE_CLIENT_SECRET") != ""
+ case ProviderGitHub:
+ return os.Getenv("GITHUB_CLIENT_ID") != "" && os.Getenv("GITHUB_CLIENT_SECRET") != ""
+ default:
+ return false
+ }
+}
diff --git a/internal/auth/session.go b/internal/auth/session.go
new file mode 100644
index 00000000..72b45364
--- /dev/null
+++ b/internal/auth/session.go
@@ -0,0 +1,171 @@
+package auth
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/bborn/workflow/internal/hostdb"
+)
+
+const (
+ // SessionCookieName is the name of the session cookie.
+ SessionCookieName = "taskyou_session"
+
+ // OAuthStateCookieName is the name of the OAuth state cookie.
+ OAuthStateCookieName = "taskyou_oauth_state"
+)
+
+// SessionManager handles session creation and validation.
+type SessionManager struct {
+ db *hostdb.DB
+ secure bool // Use secure cookies (HTTPS only)
+ domain string
+ maxAge time.Duration
+}
+
+// NewSessionManager creates a new session manager.
+func NewSessionManager(db *hostdb.DB, secure bool, domain string) *SessionManager {
+ return &SessionManager{
+ db: db,
+ secure: secure,
+ domain: domain,
+ maxAge: hostdb.DefaultSessionDuration,
+ }
+}
+
+// CreateSession creates a new session and sets the session cookie.
+func (sm *SessionManager) CreateSession(w http.ResponseWriter, userID string) (*hostdb.Session, error) {
+ session, err := sm.db.CreateSession(userID, sm.maxAge)
+ if err != nil {
+ return nil, fmt.Errorf("create session: %w", err)
+ }
+
+ sm.setSessionCookie(w, session.ID, session.ExpiresAt)
+ return session, nil
+}
+
+// GetSession retrieves the session from the request cookie.
+// Returns nil if no valid session exists.
+func (sm *SessionManager) GetSession(r *http.Request) (*hostdb.Session, error) {
+ cookie, err := r.Cookie(SessionCookieName)
+ if err != nil {
+ return nil, nil // No cookie, no session
+ }
+
+ session, err := sm.db.GetSession(cookie.Value)
+ if err != nil {
+ return nil, fmt.Errorf("get session: %w", err)
+ }
+
+ return session, nil
+}
+
+// GetSessionWithUser retrieves the session and user from the request cookie.
+// Returns nil for both if no valid session exists.
+func (sm *SessionManager) GetSessionWithUser(r *http.Request) (*hostdb.Session, *hostdb.User, error) {
+ cookie, err := r.Cookie(SessionCookieName)
+ if err != nil {
+ return nil, nil, nil // No cookie, no session
+ }
+
+ session, user, err := sm.db.GetSessionWithUser(cookie.Value)
+ if err != nil {
+ return nil, nil, fmt.Errorf("get session with user: %w", err)
+ }
+
+ return session, user, nil
+}
+
+// DeleteSession deletes the session and clears the cookie.
+func (sm *SessionManager) DeleteSession(w http.ResponseWriter, r *http.Request) error {
+ cookie, err := r.Cookie(SessionCookieName)
+ if err != nil {
+ return nil // No cookie to delete
+ }
+
+ if err := sm.db.DeleteSession(cookie.Value); err != nil {
+ return fmt.Errorf("delete session: %w", err)
+ }
+
+ sm.clearSessionCookie(w)
+ return nil
+}
+
+// setSessionCookie sets the session cookie.
+func (sm *SessionManager) setSessionCookie(w http.ResponseWriter, sessionID string, expiresAt time.Time) {
+ http.SetCookie(w, &http.Cookie{
+ Name: SessionCookieName,
+ Value: sessionID,
+ Path: "/",
+ Domain: sm.domain,
+ Expires: expiresAt,
+ MaxAge: int(time.Until(expiresAt).Seconds()),
+ HttpOnly: true,
+ Secure: sm.secure,
+ SameSite: http.SameSiteLaxMode, // Lax for OAuth redirects to work
+ })
+}
+
+// clearSessionCookie clears the session cookie.
+func (sm *SessionManager) clearSessionCookie(w http.ResponseWriter) {
+ http.SetCookie(w, &http.Cookie{
+ Name: SessionCookieName,
+ Value: "",
+ Path: "/",
+ Domain: sm.domain,
+ Expires: time.Unix(0, 0),
+ MaxAge: -1,
+ HttpOnly: true,
+ Secure: sm.secure,
+ SameSite: http.SameSiteLaxMode,
+ })
+}
+
+// GenerateOAuthState generates a random state string for OAuth.
+func GenerateOAuthState() (string, error) {
+ b := make([]byte, 16)
+ if _, err := rand.Read(b); err != nil {
+ return "", fmt.Errorf("generate random bytes: %w", err)
+ }
+ return base64.URLEncoding.EncodeToString(b), nil
+}
+
+// SetOAuthStateCookie sets the OAuth state cookie.
+func (sm *SessionManager) SetOAuthStateCookie(w http.ResponseWriter, state string) {
+ http.SetCookie(w, &http.Cookie{
+ Name: OAuthStateCookieName,
+ Value: state,
+ Path: "/",
+ Domain: sm.domain,
+ MaxAge: 600, // 10 minutes
+ HttpOnly: true,
+ Secure: sm.secure,
+ SameSite: http.SameSiteLaxMode,
+ })
+}
+
+// GetOAuthStateCookie retrieves and clears the OAuth state cookie.
+func (sm *SessionManager) GetOAuthStateCookie(w http.ResponseWriter, r *http.Request) string {
+ cookie, err := r.Cookie(OAuthStateCookieName)
+ if err != nil {
+ return ""
+ }
+
+ // Clear the cookie
+ http.SetCookie(w, &http.Cookie{
+ Name: OAuthStateCookieName,
+ Value: "",
+ Path: "/",
+ Domain: sm.domain,
+ Expires: time.Unix(0, 0),
+ MaxAge: -1,
+ HttpOnly: true,
+ Secure: sm.secure,
+ SameSite: http.SameSiteLaxMode,
+ })
+
+ return cookie.Value
+}
diff --git a/internal/db/tasks.go b/internal/db/tasks.go
index 1afb38c1..6e71e261 100644
--- a/internal/db/tasks.go
+++ b/internal/db/tasks.go
@@ -500,6 +500,18 @@ func (db *DB) UpdateTaskPaneIDs(taskID int64, claudePaneID, shellPaneID string)
return nil
}
+// UpdateTaskSchedule updates the scheduled_at, recurrence, and last_run_at for a task.
+func (db *DB) UpdateTaskSchedule(taskID int64, scheduledAt *LocalTime, recurrence string, lastRunAt *LocalTime) error {
+ _, err := db.Exec(`
+ UPDATE tasks SET scheduled_at = ?, recurrence = ?, last_run_at = ?, updated_at = CURRENT_TIMESTAMP
+ WHERE id = ?
+ `, scheduledAt, recurrence, lastRunAt, taskID)
+ if err != nil {
+ return fmt.Errorf("update task schedule: %w", err)
+ }
+ return nil
+}
+
// DeleteTask deletes a task.
func (db *DB) DeleteTask(id int64) error {
_, err := db.Exec("DELETE FROM tasks WHERE id = ?", id)
diff --git a/internal/hostdb/db.go b/internal/hostdb/db.go
new file mode 100644
index 00000000..3747636b
--- /dev/null
+++ b/internal/hostdb/db.go
@@ -0,0 +1,135 @@
+// Package hostdb provides SQLite database operations for the web host service.
+// This database stores only authentication data, user accounts, and sprite mappings.
+// User task data is stored separately in each user's Fly Sprite.
+package hostdb
+
+import (
+ "database/sql"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ _ "modernc.org/sqlite"
+)
+
+// DB wraps the SQLite database connection for the host database.
+type DB struct {
+ *sql.DB
+ path string
+}
+
+// Path returns the path to the database file.
+func (db *DB) Path() string {
+ return db.path
+}
+
+// Open opens or creates a SQLite database at the given path.
+func Open(path string) (*DB, error) {
+ // Ensure directory exists
+ dir := filepath.Dir(path)
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return nil, fmt.Errorf("create db directory: %w", err)
+ }
+
+ // Add busy timeout to handle concurrent access
+ dsn := path + "?_busy_timeout=5000"
+ db, err := sql.Open("sqlite", dsn)
+ if err != nil {
+ return nil, fmt.Errorf("open database: %w", err)
+ }
+
+ // Enable WAL mode for better concurrent access
+ if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
+ return nil, fmt.Errorf("enable WAL: %w", err)
+ }
+
+ // Enable foreign keys
+ if _, err := db.Exec("PRAGMA foreign_keys=ON"); err != nil {
+ return nil, fmt.Errorf("enable foreign keys: %w", err)
+ }
+
+ wrapped := &DB{DB: db, path: path}
+
+ // Run migrations
+ if err := wrapped.migrate(); err != nil {
+ return nil, fmt.Errorf("migrate: %w", err)
+ }
+
+ return wrapped, nil
+}
+
+// migrate runs database migrations.
+func (db *DB) migrate() error {
+ migrations := []string{
+ // Users table - authenticated users
+ `CREATE TABLE IF NOT EXISTS users (
+ id TEXT PRIMARY KEY,
+ email TEXT NOT NULL UNIQUE,
+ name TEXT DEFAULT '',
+ avatar_url TEXT DEFAULT '',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ )`,
+
+ // OAuth accounts linked to users
+ `CREATE TABLE IF NOT EXISTS oauth_accounts (
+ id TEXT PRIMARY KEY,
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ provider TEXT NOT NULL,
+ provider_account_id TEXT NOT NULL,
+ access_token TEXT DEFAULT '',
+ refresh_token TEXT DEFAULT '',
+ expires_at DATETIME,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE(provider, provider_account_id)
+ )`,
+
+ // Sprite instances per user
+ `CREATE TABLE IF NOT EXISTS sprites (
+ id TEXT PRIMARY KEY,
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ sprite_name TEXT NOT NULL UNIQUE,
+ status TEXT DEFAULT 'pending',
+ region TEXT DEFAULT 'ord',
+ ssh_public_key TEXT DEFAULT '',
+ last_checkpoint TEXT DEFAULT '',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ )`,
+
+ // Sessions for cookie-based session management
+ `CREATE TABLE IF NOT EXISTS sessions (
+ id TEXT PRIMARY KEY,
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ expires_at DATETIME NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ )`,
+
+ // Indexes
+ `CREATE INDEX IF NOT EXISTS idx_oauth_accounts_user ON oauth_accounts(user_id)`,
+ `CREATE INDEX IF NOT EXISTS idx_oauth_accounts_provider ON oauth_accounts(provider, provider_account_id)`,
+ `CREATE INDEX IF NOT EXISTS idx_sprites_user ON sprites(user_id)`,
+ `CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id)`,
+ `CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at)`,
+ }
+
+ for _, m := range migrations {
+ if _, err := db.Exec(m); err != nil {
+ return fmt.Errorf("migration failed: %w\nSQL: %s", err, m)
+ }
+ }
+
+ return nil
+}
+
+// DefaultPath returns the default database path for the host service.
+func DefaultPath() string {
+ // Check for explicit path
+ if p := os.Getenv("TASKWEB_DATABASE_PATH"); p != "" {
+ return p
+ }
+
+ // Default to ~/.local/share/taskweb/taskweb.db
+ home, _ := os.UserHomeDir()
+ return filepath.Join(home, ".local", "share", "taskweb", "taskweb.db")
+}
diff --git a/internal/hostdb/db_test.go b/internal/hostdb/db_test.go
new file mode 100644
index 00000000..0d898522
--- /dev/null
+++ b/internal/hostdb/db_test.go
@@ -0,0 +1,245 @@
+package hostdb
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+)
+
+func setupTestDB(t *testing.T) (*DB, func()) {
+ tmpDir, err := os.MkdirTemp("", "hostdb-test-*")
+ if err != nil {
+ t.Fatalf("failed to create temp dir: %v", err)
+ }
+
+ dbPath := filepath.Join(tmpDir, "test.db")
+ db, err := Open(dbPath)
+ if err != nil {
+ os.RemoveAll(tmpDir)
+ t.Fatalf("failed to open database: %v", err)
+ }
+
+ cleanup := func() {
+ db.Close()
+ os.RemoveAll(tmpDir)
+ }
+
+ return db, cleanup
+}
+
+func TestCreateAndGetUser(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ // Create user
+ user, err := db.CreateUser("test@example.com", "Test User", "https://example.com/avatar.png")
+ if err != nil {
+ t.Fatalf("failed to create user: %v", err)
+ }
+
+ if user.Email != "test@example.com" {
+ t.Errorf("expected email test@example.com, got %s", user.Email)
+ }
+ if user.Name != "Test User" {
+ t.Errorf("expected name Test User, got %s", user.Name)
+ }
+
+ // Get by ID
+ retrieved, err := db.GetUserByID(user.ID)
+ if err != nil {
+ t.Fatalf("failed to get user by ID: %v", err)
+ }
+ if retrieved == nil {
+ t.Fatal("expected user, got nil")
+ }
+ if retrieved.Email != user.Email {
+ t.Errorf("expected email %s, got %s", user.Email, retrieved.Email)
+ }
+
+ // Get by email
+ retrieved, err = db.GetUserByEmail("test@example.com")
+ if err != nil {
+ t.Fatalf("failed to get user by email: %v", err)
+ }
+ if retrieved == nil {
+ t.Fatal("expected user, got nil")
+ }
+}
+
+func TestGetOrCreateUserByOAuth(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ expiresAt := time.Now().Add(time.Hour)
+
+ // First call should create user
+ user1, isNew1, err := db.GetOrCreateUserByOAuth(
+ "google", "12345", "test@example.com", "Test User", "https://example.com/avatar.png",
+ "access_token", "refresh_token", &expiresAt,
+ )
+ if err != nil {
+ t.Fatalf("failed to get or create user: %v", err)
+ }
+ if !isNew1 {
+ t.Error("expected new user to be created")
+ }
+ if user1.Email != "test@example.com" {
+ t.Errorf("expected email test@example.com, got %s", user1.Email)
+ }
+
+ // Second call with same OAuth should return existing user
+ user2, isNew2, err := db.GetOrCreateUserByOAuth(
+ "google", "12345", "test@example.com", "Updated Name", "https://example.com/new-avatar.png",
+ "new_access_token", "new_refresh_token", &expiresAt,
+ )
+ if err != nil {
+ t.Fatalf("failed to get or create user: %v", err)
+ }
+ if isNew2 {
+ t.Error("expected existing user, not new")
+ }
+ if user2.ID != user1.ID {
+ t.Errorf("expected same user ID %s, got %s", user1.ID, user2.ID)
+ }
+ // Name should be updated
+ if user2.Name != "Updated Name" {
+ t.Errorf("expected updated name, got %s", user2.Name)
+ }
+}
+
+func TestCreateAndGetSprite(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ // Create user first
+ user, err := db.CreateUser("test@example.com", "Test User", "")
+ if err != nil {
+ t.Fatalf("failed to create user: %v", err)
+ }
+
+ // Create sprite
+ sprite, err := db.CreateSprite(user.ID, "taskyou-abc123", "ord")
+ if err != nil {
+ t.Fatalf("failed to create sprite: %v", err)
+ }
+
+ if sprite.SpriteName != "taskyou-abc123" {
+ t.Errorf("expected sprite name taskyou-abc123, got %s", sprite.SpriteName)
+ }
+ if sprite.Status != SpriteStatusPending {
+ t.Errorf("expected status pending, got %s", sprite.Status)
+ }
+
+ // Get by user
+ retrieved, err := db.GetSpriteByUser(user.ID)
+ if err != nil {
+ t.Fatalf("failed to get sprite by user: %v", err)
+ }
+ if retrieved == nil {
+ t.Fatal("expected sprite, got nil")
+ }
+ if retrieved.ID != sprite.ID {
+ t.Errorf("expected sprite ID %s, got %s", sprite.ID, retrieved.ID)
+ }
+
+ // Update status
+ err = db.UpdateSpriteStatus(sprite.ID, SpriteStatusRunning)
+ if err != nil {
+ t.Fatalf("failed to update sprite status: %v", err)
+ }
+
+ retrieved, _ = db.GetSpriteByID(sprite.ID)
+ if retrieved.Status != SpriteStatusRunning {
+ t.Errorf("expected status running, got %s", retrieved.Status)
+ }
+}
+
+func TestSessions(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ // Create user
+ user, err := db.CreateUser("test@example.com", "Test User", "")
+ if err != nil {
+ t.Fatalf("failed to create user: %v", err)
+ }
+
+ // Create session
+ session, err := db.CreateSession(user.ID, time.Hour)
+ if err != nil {
+ t.Fatalf("failed to create session: %v", err)
+ }
+
+ if session.UserID != user.ID {
+ t.Errorf("expected user ID %s, got %s", user.ID, session.UserID)
+ }
+
+ // Get session
+ retrieved, err := db.GetSession(session.ID)
+ if err != nil {
+ t.Fatalf("failed to get session: %v", err)
+ }
+ if retrieved == nil {
+ t.Fatal("expected session, got nil")
+ }
+
+ // Get session with user
+ sess, usr, err := db.GetSessionWithUser(session.ID)
+ if err != nil {
+ t.Fatalf("failed to get session with user: %v", err)
+ }
+ if sess == nil || usr == nil {
+ t.Fatal("expected session and user")
+ }
+ if usr.ID != user.ID {
+ t.Errorf("expected user ID %s, got %s", user.ID, usr.ID)
+ }
+
+ // Delete session
+ err = db.DeleteSession(session.ID)
+ if err != nil {
+ t.Fatalf("failed to delete session: %v", err)
+ }
+
+ // Verify deleted
+ retrieved, _ = db.GetSession(session.ID)
+ if retrieved != nil {
+ t.Error("expected session to be deleted")
+ }
+}
+
+func TestExpiredSession(t *testing.T) {
+ db, cleanup := setupTestDB(t)
+ defer cleanup()
+
+ // Create user
+ user, err := db.CreateUser("test@example.com", "Test User", "")
+ if err != nil {
+ t.Fatalf("failed to create user: %v", err)
+ }
+
+ // Create already-expired session
+ session, err := db.CreateSession(user.ID, -time.Hour) // Negative duration = already expired
+ if err != nil {
+ t.Fatalf("failed to create session: %v", err)
+ }
+
+ // Should not find expired session
+ retrieved, err := db.GetSession(session.ID)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if retrieved != nil {
+ t.Error("expected nil for expired session")
+ }
+
+ // Cleanup expired sessions
+ count, err := db.CleanupExpiredSessions()
+ if err != nil {
+ t.Fatalf("failed to cleanup sessions: %v", err)
+ }
+ if count != 1 {
+ t.Errorf("expected 1 expired session cleaned up, got %d", count)
+ }
+}
diff --git a/internal/hostdb/sessions.go b/internal/hostdb/sessions.go
new file mode 100644
index 00000000..341a90e9
--- /dev/null
+++ b/internal/hostdb/sessions.go
@@ -0,0 +1,132 @@
+package hostdb
+
+import (
+ "crypto/rand"
+ "database/sql"
+ "encoding/base64"
+ "fmt"
+ "time"
+)
+
+// Session represents an authenticated user session.
+type Session struct {
+ ID string
+ UserID string
+ ExpiresAt time.Time
+ CreatedAt time.Time
+}
+
+// DefaultSessionDuration is the default duration for sessions.
+const DefaultSessionDuration = 30 * 24 * time.Hour // 30 days
+
+// generateSessionID generates a cryptographically secure session ID.
+func generateSessionID() (string, error) {
+ b := make([]byte, 32)
+ if _, err := rand.Read(b); err != nil {
+ return "", fmt.Errorf("generate random bytes: %w", err)
+ }
+ return base64.URLEncoding.EncodeToString(b), nil
+}
+
+// CreateSession creates a new session for a user.
+func (db *DB) CreateSession(userID string, duration time.Duration) (*Session, error) {
+ id, err := generateSessionID()
+ if err != nil {
+ return nil, fmt.Errorf("generate session id: %w", err)
+ }
+
+ now := time.Now()
+ expiresAt := now.Add(duration)
+
+ _, err = db.Exec(`
+ INSERT INTO sessions (id, user_id, expires_at, created_at)
+ VALUES (?, ?, ?, ?)
+ `, id, userID, expiresAt, now)
+ if err != nil {
+ return nil, fmt.Errorf("insert session: %w", err)
+ }
+
+ return &Session{
+ ID: id,
+ UserID: userID,
+ ExpiresAt: expiresAt,
+ CreatedAt: now,
+ }, nil
+}
+
+// GetSession retrieves a session by its ID.
+// Returns nil if the session doesn't exist or has expired.
+func (db *DB) GetSession(id string) (*Session, error) {
+ var s Session
+ err := db.QueryRow(`
+ SELECT id, user_id, expires_at, created_at
+ FROM sessions WHERE id = ? AND expires_at > ?
+ `, id, time.Now()).Scan(&s.ID, &s.UserID, &s.ExpiresAt, &s.CreatedAt)
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("query session: %w", err)
+ }
+ return &s, nil
+}
+
+// GetSessionWithUser retrieves a session and its associated user.
+// Returns nil for both if the session doesn't exist or has expired.
+func (db *DB) GetSessionWithUser(sessionID string) (*Session, *User, error) {
+ session, err := db.GetSession(sessionID)
+ if err != nil {
+ return nil, nil, fmt.Errorf("get session: %w", err)
+ }
+ if session == nil {
+ return nil, nil, nil
+ }
+
+ user, err := db.GetUserByID(session.UserID)
+ if err != nil {
+ return nil, nil, fmt.Errorf("get user: %w", err)
+ }
+
+ return session, user, nil
+}
+
+// DeleteSession deletes a session.
+func (db *DB) DeleteSession(id string) error {
+ _, err := db.Exec(`DELETE FROM sessions WHERE id = ?`, id)
+ if err != nil {
+ return fmt.Errorf("delete session: %w", err)
+ }
+ return nil
+}
+
+// DeleteUserSessions deletes all sessions for a user.
+func (db *DB) DeleteUserSessions(userID string) error {
+ _, err := db.Exec(`DELETE FROM sessions WHERE user_id = ?`, userID)
+ if err != nil {
+ return fmt.Errorf("delete user sessions: %w", err)
+ }
+ return nil
+}
+
+// CleanupExpiredSessions removes all expired sessions from the database.
+func (db *DB) CleanupExpiredSessions() (int64, error) {
+ result, err := db.Exec(`DELETE FROM sessions WHERE expires_at <= ?`, time.Now())
+ if err != nil {
+ return 0, fmt.Errorf("delete expired sessions: %w", err)
+ }
+ count, _ := result.RowsAffected()
+ return count, nil
+}
+
+// ExtendSession extends the expiration of a session.
+func (db *DB) ExtendSession(id string, duration time.Duration) error {
+ expiresAt := time.Now().Add(duration)
+ _, err := db.Exec(`
+ UPDATE sessions SET expires_at = ?
+ WHERE id = ?
+ `, expiresAt, id)
+ if err != nil {
+ return fmt.Errorf("extend session: %w", err)
+ }
+ return nil
+}
diff --git a/internal/hostdb/sprites.go b/internal/hostdb/sprites.go
new file mode 100644
index 00000000..561e992f
--- /dev/null
+++ b/internal/hostdb/sprites.go
@@ -0,0 +1,197 @@
+package hostdb
+
+import (
+ "database/sql"
+ "fmt"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+// SpriteStatus represents the status of a sprite instance.
+type SpriteStatus string
+
+const (
+ SpriteStatusPending SpriteStatus = "pending"
+ SpriteStatusCreating SpriteStatus = "creating"
+ SpriteStatusRunning SpriteStatus = "running"
+ SpriteStatusStopped SpriteStatus = "stopped"
+ SpriteStatusError SpriteStatus = "error"
+)
+
+// Sprite represents a Fly Sprite instance for a user.
+type Sprite struct {
+ ID string
+ UserID string
+ SpriteName string // Fly sprite name (e.g., "taskyou-abc123")
+ Status SpriteStatus
+ Region string // Fly region (e.g., "ord")
+ SSHPublicKey string
+ LastCheckpoint string // Last checkpoint ID for restore
+ CreatedAt time.Time
+ UpdatedAt time.Time
+}
+
+// CreateSprite creates a new sprite record for a user.
+func (db *DB) CreateSprite(userID, spriteName, region string) (*Sprite, error) {
+ id := uuid.New().String()
+ now := time.Now()
+
+ _, err := db.Exec(`
+ INSERT INTO sprites (id, user_id, sprite_name, status, region, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ `, id, userID, spriteName, SpriteStatusPending, region, now, now)
+ if err != nil {
+ return nil, fmt.Errorf("insert sprite: %w", err)
+ }
+
+ return &Sprite{
+ ID: id,
+ UserID: userID,
+ SpriteName: spriteName,
+ Status: SpriteStatusPending,
+ Region: region,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }, nil
+}
+
+// GetSpriteByID retrieves a sprite by its ID.
+func (db *DB) GetSpriteByID(id string) (*Sprite, error) {
+ var s Sprite
+ err := db.QueryRow(`
+ SELECT id, user_id, sprite_name, status, region, ssh_public_key, last_checkpoint, created_at, updated_at
+ FROM sprites WHERE id = ?
+ `, id).Scan(&s.ID, &s.UserID, &s.SpriteName, &s.Status, &s.Region, &s.SSHPublicKey, &s.LastCheckpoint, &s.CreatedAt, &s.UpdatedAt)
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("query sprite: %w", err)
+ }
+ return &s, nil
+}
+
+// GetSpriteByUser retrieves the sprite for a user.
+// Each user has at most one sprite.
+func (db *DB) GetSpriteByUser(userID string) (*Sprite, error) {
+ var s Sprite
+ err := db.QueryRow(`
+ SELECT id, user_id, sprite_name, status, region, ssh_public_key, last_checkpoint, created_at, updated_at
+ FROM sprites WHERE user_id = ?
+ `, userID).Scan(&s.ID, &s.UserID, &s.SpriteName, &s.Status, &s.Region, &s.SSHPublicKey, &s.LastCheckpoint, &s.CreatedAt, &s.UpdatedAt)
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("query sprite: %w", err)
+ }
+ return &s, nil
+}
+
+// GetSpriteByName retrieves a sprite by its Fly sprite name.
+func (db *DB) GetSpriteByName(spriteName string) (*Sprite, error) {
+ var s Sprite
+ err := db.QueryRow(`
+ SELECT id, user_id, sprite_name, status, region, ssh_public_key, last_checkpoint, created_at, updated_at
+ FROM sprites WHERE sprite_name = ?
+ `, spriteName).Scan(&s.ID, &s.UserID, &s.SpriteName, &s.Status, &s.Region, &s.SSHPublicKey, &s.LastCheckpoint, &s.CreatedAt, &s.UpdatedAt)
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("query sprite: %w", err)
+ }
+ return &s, nil
+}
+
+// UpdateSpriteStatus updates the status of a sprite.
+func (db *DB) UpdateSpriteStatus(id string, status SpriteStatus) error {
+ _, err := db.Exec(`
+ UPDATE sprites SET status = ?, updated_at = ?
+ WHERE id = ?
+ `, status, time.Now(), id)
+ if err != nil {
+ return fmt.Errorf("update sprite status: %w", err)
+ }
+ return nil
+}
+
+// UpdateSpriteSSHKey updates the SSH public key for a sprite.
+func (db *DB) UpdateSpriteSSHKey(id, sshPublicKey string) error {
+ _, err := db.Exec(`
+ UPDATE sprites SET ssh_public_key = ?, updated_at = ?
+ WHERE id = ?
+ `, sshPublicKey, time.Now(), id)
+ if err != nil {
+ return fmt.Errorf("update sprite ssh key: %w", err)
+ }
+ return nil
+}
+
+// UpdateSpriteCheckpoint updates the last checkpoint ID for a sprite.
+func (db *DB) UpdateSpriteCheckpoint(id, checkpointID string) error {
+ _, err := db.Exec(`
+ UPDATE sprites SET last_checkpoint = ?, updated_at = ?
+ WHERE id = ?
+ `, checkpointID, time.Now(), id)
+ if err != nil {
+ return fmt.Errorf("update sprite checkpoint: %w", err)
+ }
+ return nil
+}
+
+// DeleteSprite deletes a sprite record.
+func (db *DB) DeleteSprite(id string) error {
+ _, err := db.Exec(`DELETE FROM sprites WHERE id = ?`, id)
+ if err != nil {
+ return fmt.Errorf("delete sprite: %w", err)
+ }
+ return nil
+}
+
+// ListSprites retrieves all sprites.
+func (db *DB) ListSprites() ([]*Sprite, error) {
+ rows, err := db.Query(`
+ SELECT id, user_id, sprite_name, status, region, ssh_public_key, last_checkpoint, created_at, updated_at
+ FROM sprites ORDER BY created_at DESC
+ `)
+ if err != nil {
+ return nil, fmt.Errorf("query sprites: %w", err)
+ }
+ defer rows.Close()
+
+ var sprites []*Sprite
+ for rows.Next() {
+ var s Sprite
+ if err := rows.Scan(&s.ID, &s.UserID, &s.SpriteName, &s.Status, &s.Region, &s.SSHPublicKey, &s.LastCheckpoint, &s.CreatedAt, &s.UpdatedAt); err != nil {
+ return nil, fmt.Errorf("scan sprite: %w", err)
+ }
+ sprites = append(sprites, &s)
+ }
+
+ return sprites, nil
+}
+
+// ListSpritesByStatus retrieves all sprites with a specific status.
+func (db *DB) ListSpritesByStatus(status SpriteStatus) ([]*Sprite, error) {
+ rows, err := db.Query(`
+ SELECT id, user_id, sprite_name, status, region, ssh_public_key, last_checkpoint, created_at, updated_at
+ FROM sprites WHERE status = ? ORDER BY created_at DESC
+ `, status)
+ if err != nil {
+ return nil, fmt.Errorf("query sprites: %w", err)
+ }
+ defer rows.Close()
+
+ var sprites []*Sprite
+ for rows.Next() {
+ var s Sprite
+ if err := rows.Scan(&s.ID, &s.UserID, &s.SpriteName, &s.Status, &s.Region, &s.SSHPublicKey, &s.LastCheckpoint, &s.CreatedAt, &s.UpdatedAt); err != nil {
+ return nil, fmt.Errorf("scan sprite: %w", err)
+ }
+ sprites = append(sprites, &s)
+ }
+
+ return sprites, nil
+}
diff --git a/internal/hostdb/users.go b/internal/hostdb/users.go
new file mode 100644
index 00000000..d4aa14fa
--- /dev/null
+++ b/internal/hostdb/users.go
@@ -0,0 +1,265 @@
+package hostdb
+
+import (
+ "database/sql"
+ "fmt"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+// User represents an authenticated user.
+type User struct {
+ ID string
+ Email string
+ Name string
+ AvatarURL string
+ CreatedAt time.Time
+ UpdatedAt time.Time
+}
+
+// OAuthAccount represents a linked OAuth provider account.
+type OAuthAccount struct {
+ ID string
+ UserID string
+ Provider string // "google", "github"
+ ProviderAccountID string
+ AccessToken string
+ RefreshToken string
+ ExpiresAt *time.Time
+ CreatedAt time.Time
+}
+
+// CreateUser creates a new user and returns the created user.
+func (db *DB) CreateUser(email, name, avatarURL string) (*User, error) {
+ id := uuid.New().String()
+ now := time.Now()
+
+ _, err := db.Exec(`
+ INSERT INTO users (id, email, name, avatar_url, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?)
+ `, id, email, name, avatarURL, now, now)
+ if err != nil {
+ return nil, fmt.Errorf("insert user: %w", err)
+ }
+
+ return &User{
+ ID: id,
+ Email: email,
+ Name: name,
+ AvatarURL: avatarURL,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }, nil
+}
+
+// GetUserByID retrieves a user by their ID.
+func (db *DB) GetUserByID(id string) (*User, error) {
+ var u User
+ err := db.QueryRow(`
+ SELECT id, email, name, avatar_url, created_at, updated_at
+ FROM users WHERE id = ?
+ `, id).Scan(&u.ID, &u.Email, &u.Name, &u.AvatarURL, &u.CreatedAt, &u.UpdatedAt)
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("query user: %w", err)
+ }
+ return &u, nil
+}
+
+// GetUserByEmail retrieves a user by their email.
+func (db *DB) GetUserByEmail(email string) (*User, error) {
+ var u User
+ err := db.QueryRow(`
+ SELECT id, email, name, avatar_url, created_at, updated_at
+ FROM users WHERE email = ?
+ `, email).Scan(&u.ID, &u.Email, &u.Name, &u.AvatarURL, &u.CreatedAt, &u.UpdatedAt)
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("query user: %w", err)
+ }
+ return &u, nil
+}
+
+// UpdateUser updates a user's profile information.
+func (db *DB) UpdateUser(id, name, avatarURL string) error {
+ _, err := db.Exec(`
+ UPDATE users SET name = ?, avatar_url = ?, updated_at = ?
+ WHERE id = ?
+ `, name, avatarURL, time.Now(), id)
+ if err != nil {
+ return fmt.Errorf("update user: %w", err)
+ }
+ return nil
+}
+
+// DeleteUser deletes a user and all associated data.
+func (db *DB) DeleteUser(id string) error {
+ _, err := db.Exec(`DELETE FROM users WHERE id = ?`, id)
+ if err != nil {
+ return fmt.Errorf("delete user: %w", err)
+ }
+ return nil
+}
+
+// CreateOAuthAccount creates a new OAuth account link for a user.
+func (db *DB) CreateOAuthAccount(userID, provider, providerAccountID, accessToken, refreshToken string, expiresAt *time.Time) (*OAuthAccount, error) {
+ id := uuid.New().String()
+ now := time.Now()
+
+ _, err := db.Exec(`
+ INSERT INTO oauth_accounts (id, user_id, provider, provider_account_id, access_token, refresh_token, expires_at, created_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ `, id, userID, provider, providerAccountID, accessToken, refreshToken, expiresAt, now)
+ if err != nil {
+ return nil, fmt.Errorf("insert oauth account: %w", err)
+ }
+
+ return &OAuthAccount{
+ ID: id,
+ UserID: userID,
+ Provider: provider,
+ ProviderAccountID: providerAccountID,
+ AccessToken: accessToken,
+ RefreshToken: refreshToken,
+ ExpiresAt: expiresAt,
+ CreatedAt: now,
+ }, nil
+}
+
+// GetOAuthAccount retrieves an OAuth account by provider and provider account ID.
+func (db *DB) GetOAuthAccount(provider, providerAccountID string) (*OAuthAccount, error) {
+ var oa OAuthAccount
+ var expiresAt sql.NullTime
+
+ err := db.QueryRow(`
+ SELECT id, user_id, provider, provider_account_id, access_token, refresh_token, expires_at, created_at
+ FROM oauth_accounts WHERE provider = ? AND provider_account_id = ?
+ `, provider, providerAccountID).Scan(
+ &oa.ID, &oa.UserID, &oa.Provider, &oa.ProviderAccountID,
+ &oa.AccessToken, &oa.RefreshToken, &expiresAt, &oa.CreatedAt,
+ )
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("query oauth account: %w", err)
+ }
+
+ if expiresAt.Valid {
+ oa.ExpiresAt = &expiresAt.Time
+ }
+
+ return &oa, nil
+}
+
+// UpdateOAuthTokens updates the tokens for an OAuth account.
+func (db *DB) UpdateOAuthTokens(id, accessToken, refreshToken string, expiresAt *time.Time) error {
+ _, err := db.Exec(`
+ UPDATE oauth_accounts SET access_token = ?, refresh_token = ?, expires_at = ?
+ WHERE id = ?
+ `, accessToken, refreshToken, expiresAt, id)
+ if err != nil {
+ return fmt.Errorf("update oauth tokens: %w", err)
+ }
+ return nil
+}
+
+// GetOAuthAccountsByUser retrieves all OAuth accounts for a user.
+func (db *DB) GetOAuthAccountsByUser(userID string) ([]*OAuthAccount, error) {
+ rows, err := db.Query(`
+ SELECT id, user_id, provider, provider_account_id, access_token, refresh_token, expires_at, created_at
+ FROM oauth_accounts WHERE user_id = ?
+ `, userID)
+ if err != nil {
+ return nil, fmt.Errorf("query oauth accounts: %w", err)
+ }
+ defer rows.Close()
+
+ var accounts []*OAuthAccount
+ for rows.Next() {
+ var oa OAuthAccount
+ var expiresAt sql.NullTime
+
+ if err := rows.Scan(
+ &oa.ID, &oa.UserID, &oa.Provider, &oa.ProviderAccountID,
+ &oa.AccessToken, &oa.RefreshToken, &expiresAt, &oa.CreatedAt,
+ ); err != nil {
+ return nil, fmt.Errorf("scan oauth account: %w", err)
+ }
+
+ if expiresAt.Valid {
+ oa.ExpiresAt = &expiresAt.Time
+ }
+
+ accounts = append(accounts, &oa)
+ }
+
+ return accounts, nil
+}
+
+// GetOrCreateUserByOAuth gets or creates a user based on OAuth login.
+// Returns the user and a boolean indicating if the user was newly created.
+func (db *DB) GetOrCreateUserByOAuth(provider, providerAccountID, email, name, avatarURL, accessToken, refreshToken string, expiresAt *time.Time) (*User, bool, error) {
+ // Check if OAuth account already exists
+ oa, err := db.GetOAuthAccount(provider, providerAccountID)
+ if err != nil {
+ return nil, false, fmt.Errorf("get oauth account: %w", err)
+ }
+
+ if oa != nil {
+ // Account exists, update tokens and return user
+ if err := db.UpdateOAuthTokens(oa.ID, accessToken, refreshToken, expiresAt); err != nil {
+ return nil, false, fmt.Errorf("update oauth tokens: %w", err)
+ }
+
+ user, err := db.GetUserByID(oa.UserID)
+ if err != nil {
+ return nil, false, fmt.Errorf("get user: %w", err)
+ }
+
+ // Update user profile if changed
+ if user.Name != name || user.AvatarURL != avatarURL {
+ if err := db.UpdateUser(user.ID, name, avatarURL); err != nil {
+ return nil, false, fmt.Errorf("update user: %w", err)
+ }
+ user.Name = name
+ user.AvatarURL = avatarURL
+ }
+
+ return user, false, nil
+ }
+
+ // Check if user with this email already exists
+ user, err := db.GetUserByEmail(email)
+ if err != nil {
+ return nil, false, fmt.Errorf("get user by email: %w", err)
+ }
+
+ if user != nil {
+ // User exists, link OAuth account
+ _, err = db.CreateOAuthAccount(user.ID, provider, providerAccountID, accessToken, refreshToken, expiresAt)
+ if err != nil {
+ return nil, false, fmt.Errorf("create oauth account: %w", err)
+ }
+ return user, false, nil
+ }
+
+ // Create new user
+ user, err = db.CreateUser(email, name, avatarURL)
+ if err != nil {
+ return nil, false, fmt.Errorf("create user: %w", err)
+ }
+
+ // Create OAuth account link
+ _, err = db.CreateOAuthAccount(user.ID, provider, providerAccountID, accessToken, refreshToken, expiresAt)
+ if err != nil {
+ return nil, false, fmt.Errorf("create oauth account: %w", err)
+ }
+
+ return user, true, nil
+}
diff --git a/internal/sprite/client.go b/internal/sprite/client.go
new file mode 100644
index 00000000..6f9114cd
--- /dev/null
+++ b/internal/sprite/client.go
@@ -0,0 +1,115 @@
+// Package sprite provides Fly Sprites integration for isolated user environments.
+package sprite
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+
+ sprites "github.com/superfly/sprites-go"
+)
+
+// Client wraps the Fly Sprites SDK client.
+type Client struct {
+ client *sprites.Client
+}
+
+// NewClient creates a new Sprites client.
+// Uses SPRITES_TOKEN environment variable for authentication.
+func NewClient() (*Client, error) {
+ token := os.Getenv("SPRITES_TOKEN")
+ if token == "" {
+ return nil, fmt.Errorf("SPRITES_TOKEN environment variable not set")
+ }
+
+ client := sprites.New(token)
+ return &Client{client: client}, nil
+}
+
+// NewClientWithToken creates a new Sprites client with an explicit token.
+func NewClientWithToken(token string) *Client {
+ return &Client{client: sprites.New(token)}
+}
+
+// Sprite returns a handle to a specific sprite.
+func (c *Client) Sprite(name string) *sprites.Sprite {
+ return c.client.Sprite(name)
+}
+
+// CreateSprite creates a new sprite with the given name.
+func (c *Client) CreateSprite(ctx context.Context, name string, opts *CreateOptions) error {
+ sprite := c.client.Sprite(name)
+
+ // Start the sprite (this creates it if it doesn't exist)
+ cmd := sprite.CommandContext(ctx, "true") // Just run true to initialize
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("create sprite: %w", err)
+ }
+
+ return nil
+}
+
+// CreateOptions contains options for creating a sprite.
+type CreateOptions struct {
+ Region string
+}
+
+// DestroySprite destroys a sprite.
+func (c *Client) DestroySprite(ctx context.Context, name string) error {
+ // The sprites-go SDK handles destruction through the API
+ // For now, we'll use a direct HTTP call since the SDK may not expose this
+ req, err := http.NewRequestWithContext(ctx, "DELETE", fmt.Sprintf("https://api.sprites.dev/v1/sprites/%s", name), nil)
+ if err != nil {
+ return fmt.Errorf("create request: %w", err)
+ }
+
+ req.Header.Set("Authorization", "Bearer "+os.Getenv("SPRITES_TOKEN"))
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return fmt.Errorf("destroy sprite: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusNotFound {
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("destroy sprite failed: %s - %s", resp.Status, string(body))
+ }
+
+ return nil
+}
+
+// RunCommand runs a command on a sprite and returns its output.
+func (c *Client) RunCommand(ctx context.Context, name string, command string, args ...string) ([]byte, error) {
+ sprite := c.client.Sprite(name)
+ cmd := sprite.CommandContext(ctx, command, args...)
+ return cmd.Output()
+}
+
+// RunCommandCombined runs a command on a sprite and returns combined stdout/stderr.
+func (c *Client) RunCommandCombined(ctx context.Context, name string, command string, args ...string) ([]byte, error) {
+ sprite := c.client.Sprite(name)
+ cmd := sprite.CommandContext(ctx, command, args...)
+ return cmd.CombinedOutput()
+}
+
+// StartCommand starts a command on a sprite without waiting for completion.
+func (c *Client) StartCommand(ctx context.Context, name string, command string, args ...string) error {
+ sprite := c.client.Sprite(name)
+ cmd := sprite.CommandContext(ctx, command, args...)
+ return cmd.Start()
+}
+
+// GetSpriteStatus checks if a sprite is running.
+func (c *Client) GetSpriteStatus(ctx context.Context, name string) (string, error) {
+ // Try to run a simple command to check if sprite is accessible
+ sprite := c.client.Sprite(name)
+ cmd := sprite.CommandContext(ctx, "echo", "ok")
+ _, err := cmd.Output()
+ if err != nil {
+ return "stopped", nil
+ }
+ return "running", nil
+}
diff --git a/internal/sprite/manager.go b/internal/sprite/manager.go
new file mode 100644
index 00000000..aef4e832
--- /dev/null
+++ b/internal/sprite/manager.go
@@ -0,0 +1,222 @@
+package sprite
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/hex"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/bborn/workflow/internal/hostdb"
+)
+
+// Manager handles sprite lifecycle management.
+type Manager struct {
+ client *Client
+ db *hostdb.DB
+}
+
+// NewManager creates a new sprite manager.
+func NewManager(client *Client, db *hostdb.DB) *Manager {
+ return &Manager{
+ client: client,
+ db: db,
+ }
+}
+
+// generateSpriteName generates a unique sprite name for a user.
+func generateSpriteName() string {
+ b := make([]byte, 6)
+ rand.Read(b)
+ return fmt.Sprintf("taskyou-%s", hex.EncodeToString(b))
+}
+
+// ProvisionSprite provisions a new sprite for a user.
+// If the user already has a sprite, returns the existing one.
+func (m *Manager) ProvisionSprite(ctx context.Context, userID string) (*hostdb.Sprite, error) {
+ // Check if user already has a sprite
+ existing, err := m.db.GetSpriteByUser(userID)
+ if err != nil {
+ return nil, fmt.Errorf("get existing sprite: %w", err)
+ }
+ if existing != nil {
+ return existing, nil
+ }
+
+ // Create new sprite record
+ spriteName := generateSpriteName()
+ region := "ord" // Default to Chicago
+
+ sprite, err := m.db.CreateSprite(userID, spriteName, region)
+ if err != nil {
+ return nil, fmt.Errorf("create sprite record: %w", err)
+ }
+
+ // Update status to creating
+ if err := m.db.UpdateSpriteStatus(sprite.ID, hostdb.SpriteStatusCreating); err != nil {
+ return nil, fmt.Errorf("update sprite status: %w", err)
+ }
+
+ // Create the actual Fly Sprite
+ if err := m.client.CreateSprite(ctx, spriteName, &CreateOptions{Region: region}); err != nil {
+ m.db.UpdateSpriteStatus(sprite.ID, hostdb.SpriteStatusError)
+ return nil, fmt.Errorf("create fly sprite: %w", err)
+ }
+
+ // Initialize the sprite with taskd
+ if err := m.initializeSprite(ctx, spriteName); err != nil {
+ m.db.UpdateSpriteStatus(sprite.ID, hostdb.SpriteStatusError)
+ return nil, fmt.Errorf("initialize sprite: %w", err)
+ }
+
+ // Update status to running
+ if err := m.db.UpdateSpriteStatus(sprite.ID, hostdb.SpriteStatusRunning); err != nil {
+ return nil, fmt.Errorf("update sprite status: %w", err)
+ }
+
+ sprite.Status = hostdb.SpriteStatusRunning
+ return sprite, nil
+}
+
+// initializeSprite sets up a sprite with taskd and required dependencies.
+func (m *Manager) initializeSprite(ctx context.Context, spriteName string) error {
+ // Install taskd binary and start the web API
+ // The sprite image should have taskd pre-installed, but we ensure it's running
+ commands := []struct {
+ name string
+ args []string
+ }{
+ // Ensure data directory exists
+ {"mkdir", []string{"-p", "/data"}},
+ // Start taskd with web API in background
+ // Note: In production, this would be managed by a process supervisor
+ {"sh", []string{"-c", "nohup /usr/local/bin/taskd --web-api --addr :8080 > /var/log/taskd.log 2>&1 &"}},
+ }
+
+ for _, cmd := range commands {
+ if _, err := m.client.RunCommandCombined(ctx, spriteName, cmd.name, cmd.args...); err != nil {
+ // Log but don't fail - some commands might fail on subsequent runs
+ continue
+ }
+ }
+
+ // Wait for taskd to be ready
+ for i := 0; i < 30; i++ {
+ output, err := m.client.RunCommand(ctx, spriteName, "curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "http://localhost:8080/health")
+ if err == nil && strings.TrimSpace(string(output)) == "200" {
+ return nil
+ }
+ time.Sleep(time.Second)
+ }
+
+ return fmt.Errorf("taskd did not become ready in time")
+}
+
+// GetUserSprite retrieves the sprite for a user.
+func (m *Manager) GetUserSprite(ctx context.Context, userID string) (*hostdb.Sprite, error) {
+ sprite, err := m.db.GetSpriteByUser(userID)
+ if err != nil {
+ return nil, fmt.Errorf("get sprite: %w", err)
+ }
+ return sprite, nil
+}
+
+// GetSpriteStatus checks the current status of a sprite.
+func (m *Manager) GetSpriteStatus(ctx context.Context, sprite *hostdb.Sprite) (hostdb.SpriteStatus, error) {
+ status, err := m.client.GetSpriteStatus(ctx, sprite.SpriteName)
+ if err != nil {
+ return hostdb.SpriteStatusError, fmt.Errorf("get sprite status: %w", err)
+ }
+
+ switch status {
+ case "running":
+ return hostdb.SpriteStatusRunning, nil
+ case "stopped":
+ return hostdb.SpriteStatusStopped, nil
+ default:
+ return hostdb.SpriteStatusError, nil
+ }
+}
+
+// StartSprite starts a stopped sprite.
+func (m *Manager) StartSprite(ctx context.Context, sprite *hostdb.Sprite) error {
+ // Run a command to wake up the sprite
+ if err := m.client.StartCommand(ctx, sprite.SpriteName, "true"); err != nil {
+ return fmt.Errorf("start sprite: %w", err)
+ }
+
+ // Re-initialize to ensure taskd is running
+ if err := m.initializeSprite(ctx, sprite.SpriteName); err != nil {
+ return fmt.Errorf("reinitialize sprite: %w", err)
+ }
+
+ if err := m.db.UpdateSpriteStatus(sprite.ID, hostdb.SpriteStatusRunning); err != nil {
+ return fmt.Errorf("update sprite status: %w", err)
+ }
+
+ return nil
+}
+
+// StopSprite stops a running sprite.
+func (m *Manager) StopSprite(ctx context.Context, sprite *hostdb.Sprite) error {
+ // Sprites auto-stop when idle, but we can explicitly stop them
+ // by killing the taskd process
+ m.client.RunCommand(ctx, sprite.SpriteName, "pkill", "-f", "taskd")
+
+ if err := m.db.UpdateSpriteStatus(sprite.ID, hostdb.SpriteStatusStopped); err != nil {
+ return fmt.Errorf("update sprite status: %w", err)
+ }
+
+ return nil
+}
+
+// DestroySprite destroys a sprite and removes its record.
+func (m *Manager) DestroySprite(ctx context.Context, sprite *hostdb.Sprite) error {
+ // Destroy the Fly Sprite
+ if err := m.client.DestroySprite(ctx, sprite.SpriteName); err != nil {
+ return fmt.Errorf("destroy fly sprite: %w", err)
+ }
+
+ // Delete the record
+ if err := m.db.DeleteSprite(sprite.ID); err != nil {
+ return fmt.Errorf("delete sprite record: %w", err)
+ }
+
+ return nil
+}
+
+// GetSSHConfig returns SSH configuration for connecting to a sprite.
+func (m *Manager) GetSSHConfig(ctx context.Context, sprite *hostdb.Sprite) (string, error) {
+ // The SSH host is the sprite URL
+ sshHost := fmt.Sprintf("%s.sprites.dev", sprite.SpriteName)
+
+ config := fmt.Sprintf(`Host %s
+ HostName %s
+ User root
+ Port 22
+ IdentityFile ~/.ssh/id_ed25519
+`, sprite.SpriteName, sshHost)
+
+ return config, nil
+}
+
+// ExportDatabase exports the sprite's SQLite database.
+func (m *Manager) ExportDatabase(ctx context.Context, sprite *hostdb.Sprite) ([]byte, error) {
+ // Create a backup of the database
+ _, err := m.client.RunCommand(ctx, sprite.SpriteName, "sqlite3", "/data/tasks.db", ".backup /tmp/export.db")
+ if err != nil {
+ return nil, fmt.Errorf("backup database: %w", err)
+ }
+
+ // Read the backup file
+ output, err := m.client.RunCommand(ctx, sprite.SpriteName, "cat", "/tmp/export.db")
+ if err != nil {
+ return nil, fmt.Errorf("read backup: %w", err)
+ }
+
+ // Clean up
+ m.client.RunCommand(ctx, sprite.SpriteName, "rm", "/tmp/export.db")
+
+ return output, nil
+}
diff --git a/internal/sprite/proxy.go b/internal/sprite/proxy.go
new file mode 100644
index 00000000..278aeaec
--- /dev/null
+++ b/internal/sprite/proxy.go
@@ -0,0 +1,125 @@
+package sprite
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+ "strings"
+
+ "github.com/bborn/workflow/internal/hostdb"
+)
+
+// ProxyHandler handles proxying requests to user sprites.
+type ProxyHandler struct {
+ manager *Manager
+}
+
+// NewProxyHandler creates a new proxy handler.
+func NewProxyHandler(manager *Manager) *ProxyHandler {
+ return &ProxyHandler{manager: manager}
+}
+
+// GetSpriteURL returns the internal URL for a sprite's web API.
+func (p *ProxyHandler) GetSpriteURL(sprite *hostdb.Sprite) string {
+ // Sprites expose their web API on port 8080
+ // The URL format for accessing sprites via the Fly network
+ return fmt.Sprintf("http://%s.internal:8080", sprite.SpriteName)
+}
+
+// ProxyRequest proxies an HTTP request to a user's sprite.
+func (p *ProxyHandler) ProxyRequest(ctx context.Context, sprite *hostdb.Sprite, w http.ResponseWriter, r *http.Request) error {
+ target, err := url.Parse(p.GetSpriteURL(sprite))
+ if err != nil {
+ return fmt.Errorf("parse sprite url: %w", err)
+ }
+
+ proxy := httputil.NewSingleHostReverseProxy(target)
+
+ // Modify the director to strip the /api prefix and adjust headers
+ originalDirector := proxy.Director
+ proxy.Director = func(req *http.Request) {
+ originalDirector(req)
+
+ // Strip /api prefix from path (the sprite API doesn't use /api prefix)
+ req.URL.Path = strings.TrimPrefix(req.URL.Path, "/api")
+ if req.URL.Path == "" {
+ req.URL.Path = "/"
+ }
+
+ // Set forwarding headers
+ req.Header.Set("X-Forwarded-For", r.RemoteAddr)
+ req.Header.Set("X-Forwarded-Host", r.Host)
+ req.Header.Set("X-Forwarded-Proto", "https")
+
+ // Remove cookies and auth headers from proxied request
+ // (sprite API doesn't need them - we've already authenticated)
+ req.Header.Del("Cookie")
+ req.Header.Del("Authorization")
+ }
+
+ // Custom error handler
+ proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
+ http.Error(w, fmt.Sprintf("Sprite unavailable: %v", err), http.StatusBadGateway)
+ }
+
+ proxy.ServeHTTP(w, r)
+ return nil
+}
+
+// ProxyWebSocket proxies a WebSocket connection to a user's sprite.
+func (p *ProxyHandler) ProxyWebSocket(ctx context.Context, sprite *hostdb.Sprite, w http.ResponseWriter, r *http.Request) error {
+ // Get sprite internal URL
+ spriteURL := p.GetSpriteURL(sprite)
+ target, err := url.Parse(spriteURL)
+ if err != nil {
+ return fmt.Errorf("parse sprite url: %w", err)
+ }
+
+ // Change scheme to ws
+ target.Scheme = "ws"
+ target.Path = "/ws"
+
+ // Hijack the connection for WebSocket proxying
+ hijacker, ok := w.(http.Hijacker)
+ if !ok {
+ return fmt.Errorf("response writer does not support hijacking")
+ }
+
+ clientConn, _, err := hijacker.Hijack()
+ if err != nil {
+ return fmt.Errorf("hijack connection: %w", err)
+ }
+ defer clientConn.Close()
+
+ // Connect to sprite WebSocket
+ // This is a simplified implementation - in production you'd use gorilla/websocket
+ // or nhooyr.io/websocket for proper WebSocket handling
+ spriteConn, err := dialWebSocket(ctx, target.String())
+ if err != nil {
+ return fmt.Errorf("dial sprite websocket: %w", err)
+ }
+ defer spriteConn.Close()
+
+ // Bidirectional copy
+ errCh := make(chan error, 2)
+ go func() {
+ _, err := io.Copy(clientConn, spriteConn)
+ errCh <- err
+ }()
+ go func() {
+ _, err := io.Copy(spriteConn, clientConn)
+ errCh <- err
+ }()
+
+ return <-errCh
+}
+
+// dialWebSocket establishes a WebSocket connection.
+// This is a placeholder - in production, use a proper WebSocket library.
+func dialWebSocket(ctx context.Context, url string) (io.ReadWriteCloser, error) {
+ // In production, use gorilla/websocket or nhooyr.io/websocket
+ return nil, fmt.Errorf("websocket not implemented - use gorilla/websocket")
+}
diff --git a/internal/webapi/projects.go b/internal/webapi/projects.go
new file mode 100644
index 00000000..bfbd1430
--- /dev/null
+++ b/internal/webapi/projects.go
@@ -0,0 +1,237 @@
+package webapi
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/bborn/workflow/internal/db"
+)
+
+// ProjectResponse represents a project in JSON responses.
+type ProjectResponse struct {
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+ Path string `json:"path"`
+ Aliases string `json:"aliases"`
+ Instructions string `json:"instructions"`
+ Color string `json:"color"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+func projectToResponse(p *db.Project) *ProjectResponse {
+ return &ProjectResponse{
+ ID: p.ID,
+ Name: p.Name,
+ Path: p.Path,
+ Aliases: p.Aliases,
+ Instructions: p.Instructions,
+ Color: p.Color,
+ CreatedAt: p.CreatedAt.Time,
+ }
+}
+
+// handleListProjects handles GET /projects
+func (s *Server) handleListProjects(w http.ResponseWriter, r *http.Request) {
+ projects, err := s.db.ListProjects()
+ if err != nil {
+ s.logger.Error("list projects failed", "error", err)
+ jsonError(w, "Failed to list projects", http.StatusInternalServerError)
+ return
+ }
+
+ responses := make([]*ProjectResponse, len(projects))
+ for i, p := range projects {
+ responses[i] = projectToResponse(p)
+ }
+
+ jsonResponse(w, responses, http.StatusOK)
+}
+
+// CreateProjectRequest represents a request to create a project.
+type CreateProjectRequest struct {
+ Name string `json:"name"`
+ Path string `json:"path"`
+ Aliases string `json:"aliases,omitempty"`
+ Instructions string `json:"instructions,omitempty"`
+ Color string `json:"color,omitempty"`
+}
+
+// handleCreateProject handles POST /projects
+func (s *Server) handleCreateProject(w http.ResponseWriter, r *http.Request) {
+ var req CreateProjectRequest
+ if err := parseJSON(r, &req); err != nil {
+ jsonError(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ if req.Name == "" {
+ jsonError(w, "Name is required", http.StatusBadRequest)
+ return
+ }
+ if req.Path == "" {
+ jsonError(w, "Path is required", http.StatusBadRequest)
+ return
+ }
+
+ project := &db.Project{
+ Name: req.Name,
+ Path: req.Path,
+ Aliases: req.Aliases,
+ Instructions: req.Instructions,
+ Color: req.Color,
+ }
+
+ if err := s.db.CreateProject(project); err != nil {
+ s.logger.Error("create project failed", "error", err)
+ jsonError(w, "Failed to create project", http.StatusInternalServerError)
+ return
+ }
+
+ jsonResponse(w, projectToResponse(project), http.StatusCreated)
+}
+
+// handleGetProject handles GET /projects/{id}
+func (s *Server) handleGetProject(w http.ResponseWriter, r *http.Request) {
+ id, err := getIDParam(r)
+ if err != nil {
+ jsonError(w, "Invalid project ID", http.StatusBadRequest)
+ return
+ }
+
+ project, err := s.getProjectByID(id)
+ if err != nil {
+ s.logger.Error("get project failed", "error", err)
+ jsonError(w, "Failed to get project", http.StatusInternalServerError)
+ return
+ }
+
+ if project == nil {
+ jsonError(w, "Project not found", http.StatusNotFound)
+ return
+ }
+
+ jsonResponse(w, projectToResponse(project), http.StatusOK)
+}
+
+// getProjectByID retrieves a project by ID by scanning all projects.
+func (s *Server) getProjectByID(id int64) (*db.Project, error) {
+ projects, err := s.db.ListProjects()
+ if err != nil {
+ return nil, err
+ }
+ for _, p := range projects {
+ if p.ID == id {
+ return p, nil
+ }
+ }
+ return nil, nil
+}
+
+// UpdateProjectRequest represents a request to update a project.
+type UpdateProjectRequest struct {
+ Name *string `json:"name,omitempty"`
+ Path *string `json:"path,omitempty"`
+ Aliases *string `json:"aliases,omitempty"`
+ Instructions *string `json:"instructions,omitempty"`
+ Color *string `json:"color,omitempty"`
+}
+
+// handleUpdateProject handles PUT /projects/{id}
+func (s *Server) handleUpdateProject(w http.ResponseWriter, r *http.Request) {
+ id, err := getIDParam(r)
+ if err != nil {
+ jsonError(w, "Invalid project ID", http.StatusBadRequest)
+ return
+ }
+
+ var req UpdateProjectRequest
+ if err := parseJSON(r, &req); err != nil {
+ jsonError(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ project, err := s.getProjectByID(id)
+ if err != nil {
+ s.logger.Error("get project failed", "error", err)
+ jsonError(w, "Failed to get project", http.StatusInternalServerError)
+ return
+ }
+ if project == nil {
+ jsonError(w, "Project not found", http.StatusNotFound)
+ return
+ }
+
+ // Apply updates
+ if req.Name != nil {
+ project.Name = *req.Name
+ }
+ if req.Path != nil {
+ project.Path = *req.Path
+ }
+ if req.Aliases != nil {
+ project.Aliases = *req.Aliases
+ }
+ if req.Instructions != nil {
+ project.Instructions = *req.Instructions
+ }
+ if req.Color != nil {
+ project.Color = *req.Color
+ }
+
+ if err := s.db.UpdateProject(project); err != nil {
+ s.logger.Error("update project failed", "error", err)
+ jsonError(w, "Failed to update project", http.StatusInternalServerError)
+ return
+ }
+
+ jsonResponse(w, projectToResponse(project), http.StatusOK)
+}
+
+// handleDeleteProject handles DELETE /projects/{id}
+func (s *Server) handleDeleteProject(w http.ResponseWriter, r *http.Request) {
+ id, err := getIDParam(r)
+ if err != nil {
+ jsonError(w, "Invalid project ID", http.StatusBadRequest)
+ return
+ }
+
+ if err := s.db.DeleteProject(id); err != nil {
+ s.logger.Error("delete project failed", "error", err)
+ jsonError(w, "Failed to delete project", http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+}
+
+// handleGetSettings handles GET /settings
+func (s *Server) handleGetSettings(w http.ResponseWriter, r *http.Request) {
+ settings := make(map[string]string)
+
+ // Get common settings
+ keys := []string{"theme", "pane_height", "default_project", "default_type"}
+ for _, key := range keys {
+ if value, err := s.db.GetSetting(key); err == nil {
+ settings[key] = value
+ }
+ }
+
+ jsonResponse(w, settings, http.StatusOK)
+}
+
+// handleUpdateSettings handles PUT /settings
+func (s *Server) handleUpdateSettings(w http.ResponseWriter, r *http.Request) {
+ var settings map[string]string
+ if err := parseJSON(r, &settings); err != nil {
+ jsonError(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ for key, value := range settings {
+ if err := s.db.SetSetting(key, value); err != nil {
+ s.logger.Error("set setting failed", "key", key, "error", err)
+ }
+ }
+
+ jsonResponse(w, map[string]bool{"success": true}, http.StatusOK)
+}
diff --git a/internal/webapi/server.go b/internal/webapi/server.go
new file mode 100644
index 00000000..a3138db8
--- /dev/null
+++ b/internal/webapi/server.go
@@ -0,0 +1,399 @@
+// Package webapi provides the HTTP API that runs inside each user's sprite.
+// This exposes the task database and executor state via REST and WebSocket.
+package webapi
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "net"
+ "net/http"
+ "os"
+ "strconv"
+ "sync"
+ "time"
+
+ "github.com/bborn/workflow/internal/db"
+ "github.com/charmbracelet/log"
+ "github.com/gorilla/websocket"
+)
+
+// Server is the web API server that runs inside each sprite.
+type Server struct {
+ db *db.DB
+ addr string
+ logger *log.Logger
+ wsHub *WebSocketHub
+ devMode bool
+ devOrigin string
+}
+
+// Config holds server configuration.
+type Config struct {
+ Addr string
+ DB *db.DB
+ DevMode bool // Enable CORS for local development
+ DevOrigin string // Allowed origin in dev mode (e.g., "http://localhost:5173")
+}
+
+// New creates a new API server.
+func New(cfg Config) *Server {
+ return &Server{
+ db: cfg.DB,
+ addr: cfg.Addr,
+ logger: log.NewWithOptions(os.Stderr, log.Options{Prefix: "webapi"}),
+ wsHub: NewWebSocketHub(),
+ devMode: cfg.DevMode,
+ devOrigin: cfg.DevOrigin,
+ }
+}
+
+// Start starts the API server.
+func (s *Server) Start(ctx context.Context) error {
+ mux := http.NewServeMux()
+
+ // Health check
+ mux.HandleFunc("GET /health", s.handleHealth)
+
+ // Task endpoints
+ mux.HandleFunc("GET /tasks", s.handleListTasks)
+ mux.HandleFunc("POST /tasks", s.handleCreateTask)
+ mux.HandleFunc("GET /tasks/{id}", s.handleGetTask)
+ mux.HandleFunc("PUT /tasks/{id}", s.handleUpdateTask)
+ mux.HandleFunc("DELETE /tasks/{id}", s.handleDeleteTask)
+ mux.HandleFunc("POST /tasks/{id}/queue", s.handleQueueTask)
+ mux.HandleFunc("POST /tasks/{id}/retry", s.handleRetryTask)
+ mux.HandleFunc("POST /tasks/{id}/close", s.handleCloseTask)
+
+ // Project endpoints
+ mux.HandleFunc("GET /projects", s.handleListProjects)
+ mux.HandleFunc("POST /projects", s.handleCreateProject)
+ mux.HandleFunc("GET /projects/{id}", s.handleGetProject)
+ mux.HandleFunc("PUT /projects/{id}", s.handleUpdateProject)
+ mux.HandleFunc("DELETE /projects/{id}", s.handleDeleteProject)
+
+ // Settings endpoints
+ mux.HandleFunc("GET /settings", s.handleGetSettings)
+ mux.HandleFunc("PUT /settings", s.handleUpdateSettings)
+
+ // Task logs
+ mux.HandleFunc("GET /tasks/{id}/logs", s.handleGetTaskLogs)
+
+ // Terminal endpoints (ttyd)
+ mux.HandleFunc("GET /tasks/{id}/terminal", s.handleGetTerminal)
+ mux.HandleFunc("DELETE /tasks/{id}/terminal", s.handleStopTerminal)
+
+ // WebSocket
+ mux.HandleFunc("GET /ws", s.handleWebSocket)
+
+ // Apply middleware
+ var handler http.Handler = mux
+ if s.devMode {
+ handler = s.corsMiddleware(handler)
+ }
+ handler = s.loggingMiddleware(handler)
+
+ server := &http.Server{
+ Addr: s.addr,
+ Handler: handler,
+ ReadTimeout: 15 * time.Second,
+ WriteTimeout: 15 * time.Second,
+ IdleTimeout: 60 * time.Second,
+ }
+
+ // Start WebSocket hub
+ go s.wsHub.Run()
+
+ s.logger.Info("starting API server", "addr", s.addr)
+
+ // Start server in goroutine
+ errCh := make(chan error, 1)
+ go func() {
+ if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ errCh <- err
+ }
+ }()
+
+ // Wait for context cancellation or error
+ select {
+ case <-ctx.Done():
+ shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ return server.Shutdown(shutdownCtx)
+ case err := <-errCh:
+ return err
+ }
+}
+
+// corsMiddleware adds CORS headers for local development.
+func (s *Server) corsMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // In dev mode, allow the actual request origin (Vite may use different ports)
+ origin := r.Header.Get("Origin")
+ if origin == "" {
+ origin = s.devOrigin
+ }
+ if origin == "" {
+ origin = "http://localhost:5173"
+ }
+
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+
+ if r.Method == "OPTIONS" {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ next.ServeHTTP(w, r)
+ })
+}
+
+// loggingMiddleware logs HTTP requests.
+func (s *Server) loggingMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+
+ wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
+ next.ServeHTTP(wrapped, r)
+
+ s.logger.Debug("request",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "status", wrapped.statusCode,
+ "duration", time.Since(start),
+ )
+ })
+}
+
+type responseWriter struct {
+ http.ResponseWriter
+ statusCode int
+}
+
+func (rw *responseWriter) WriteHeader(code int) {
+ rw.statusCode = code
+ rw.ResponseWriter.WriteHeader(code)
+}
+
+// Hijack implements http.Hijacker for WebSocket support.
+func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+ if hijacker, ok := rw.ResponseWriter.(http.Hijacker); ok {
+ return hijacker.Hijack()
+ }
+ return nil, nil, http.ErrNotSupported
+}
+
+// JSON response helpers
+func jsonResponse(w http.ResponseWriter, data interface{}, status int) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+ json.NewEncoder(w).Encode(data)
+}
+
+func jsonError(w http.ResponseWriter, message string, status int) {
+ jsonResponse(w, map[string]string{"error": message}, status)
+}
+
+func parseJSON(r *http.Request, v interface{}) error {
+ return json.NewDecoder(r.Body).Decode(v)
+}
+
+// getIDParam extracts an ID from the URL path.
+func getIDParam(r *http.Request) (int64, error) {
+ idStr := r.PathValue("id")
+ return strconv.ParseInt(idStr, 10, 64)
+}
+
+// handleHealth handles health check requests.
+func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("ok"))
+}
+
+// BroadcastTaskUpdate broadcasts a task update to all connected WebSocket clients.
+func (s *Server) BroadcastTaskUpdate(task *db.Task) {
+ s.wsHub.Broadcast(Message{
+ Type: "task_update",
+ Data: task,
+ })
+}
+
+// BroadcastTaskLog broadcasts a task log entry to all connected WebSocket clients.
+func (s *Server) BroadcastTaskLog(taskID int64, log *db.TaskLog) {
+ s.wsHub.Broadcast(Message{
+ Type: "task_log",
+ Data: map[string]interface{}{
+ "task_id": taskID,
+ "log": log,
+ },
+ })
+}
+
+// WebSocketHub manages WebSocket connections.
+type WebSocketHub struct {
+ clients map[*WebSocketClient]bool
+ broadcast chan Message
+ register chan *WebSocketClient
+ unregister chan *WebSocketClient
+ mu sync.RWMutex
+}
+
+// WebSocketClient represents a connected WebSocket client.
+type WebSocketClient struct {
+ hub *WebSocketHub
+ conn *websocket.Conn
+ send chan []byte
+}
+
+// Message represents a WebSocket message.
+type Message struct {
+ Type string `json:"type"`
+ Data interface{} `json:"data"`
+}
+
+// NewWebSocketHub creates a new WebSocket hub.
+func NewWebSocketHub() *WebSocketHub {
+ return &WebSocketHub{
+ clients: make(map[*WebSocketClient]bool),
+ broadcast: make(chan Message),
+ register: make(chan *WebSocketClient),
+ unregister: make(chan *WebSocketClient),
+ }
+}
+
+// Run starts the WebSocket hub.
+func (h *WebSocketHub) Run() {
+ for {
+ select {
+ case client := <-h.register:
+ h.mu.Lock()
+ h.clients[client] = true
+ h.mu.Unlock()
+
+ case client := <-h.unregister:
+ h.mu.Lock()
+ if _, ok := h.clients[client]; ok {
+ delete(h.clients, client)
+ close(client.send)
+ }
+ h.mu.Unlock()
+
+ case message := <-h.broadcast:
+ data, err := json.Marshal(message)
+ if err != nil {
+ continue
+ }
+
+ h.mu.RLock()
+ for client := range h.clients {
+ select {
+ case client.send <- data:
+ default:
+ h.mu.RUnlock()
+ h.mu.Lock()
+ close(client.send)
+ delete(h.clients, client)
+ h.mu.Unlock()
+ h.mu.RLock()
+ }
+ }
+ h.mu.RUnlock()
+ }
+ }
+}
+
+// Broadcast sends a message to all connected clients.
+func (h *WebSocketHub) Broadcast(msg Message) {
+ h.broadcast <- msg
+}
+
+var upgrader = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+ CheckOrigin: func(r *http.Request) bool {
+ return true // Allow all origins (we're behind the proxy)
+ },
+}
+
+// handleWebSocket handles WebSocket connections.
+func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ s.logger.Error("websocket upgrade failed", "error", err)
+ return
+ }
+
+ client := &WebSocketClient{
+ hub: s.wsHub,
+ conn: conn,
+ send: make(chan []byte, 256),
+ }
+
+ s.wsHub.register <- client
+
+ // Start goroutines for reading and writing
+ go client.writePump()
+ go client.readPump()
+}
+
+func (c *WebSocketClient) readPump() {
+ defer func() {
+ c.hub.unregister <- c
+ c.conn.Close()
+ }()
+
+ c.conn.SetReadLimit(512 * 1024) // 512KB
+ c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
+ c.conn.SetPongHandler(func(string) error {
+ c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
+ return nil
+ })
+
+ for {
+ _, _, err := c.conn.ReadMessage()
+ if err != nil {
+ if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
+ // Log unexpected close errors
+ }
+ break
+ }
+ }
+}
+
+func (c *WebSocketClient) writePump() {
+ ticker := time.NewTicker(54 * time.Second)
+ defer func() {
+ ticker.Stop()
+ c.conn.Close()
+ }()
+
+ for {
+ select {
+ case message, ok := <-c.send:
+ c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
+ if !ok {
+ c.conn.WriteMessage(websocket.CloseMessage, []byte{})
+ return
+ }
+
+ w, err := c.conn.NextWriter(websocket.TextMessage)
+ if err != nil {
+ return
+ }
+ w.Write(message)
+
+ if err := w.Close(); err != nil {
+ return
+ }
+
+ case <-ticker.C:
+ c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
+ if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
+ return
+ }
+ }
+ }
+}
diff --git a/internal/webapi/server_test.go b/internal/webapi/server_test.go
new file mode 100644
index 00000000..960c106b
--- /dev/null
+++ b/internal/webapi/server_test.go
@@ -0,0 +1,276 @@
+package webapi
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/bborn/workflow/internal/db"
+)
+
+func setupTestServer(t *testing.T) (*Server, func()) {
+ tmpDir, err := os.MkdirTemp("", "webapi-test-*")
+ if err != nil {
+ t.Fatalf("failed to create temp dir: %v", err)
+ }
+
+ dbPath := filepath.Join(tmpDir, "test.db")
+ database, err := db.Open(dbPath)
+ if err != nil {
+ os.RemoveAll(tmpDir)
+ t.Fatalf("failed to open database: %v", err)
+ }
+
+ server := New(Config{
+ Addr: ":0",
+ DB: database,
+ })
+
+ // Start the WebSocket hub in a goroutine
+ go server.wsHub.Run()
+
+ cleanup := func() {
+ database.Close()
+ os.RemoveAll(tmpDir)
+ }
+
+ return server, cleanup
+}
+
+func TestListTasks(t *testing.T) {
+ server, cleanup := setupTestServer(t)
+ defer cleanup()
+
+ // Create a task first
+ task := &db.Task{
+ Title: "Test Task",
+ Body: "Test body",
+ Type: "code",
+ Project: "personal",
+ Status: db.StatusBacklog,
+ }
+ if err := server.db.CreateTask(task); err != nil {
+ t.Fatalf("failed to create task: %v", err)
+ }
+
+ // Test list tasks
+ req := httptest.NewRequest("GET", "/tasks", nil)
+ w := httptest.NewRecorder()
+
+ server.handleListTasks(w, req)
+
+ resp := w.Result()
+ if resp.StatusCode != http.StatusOK {
+ t.Errorf("expected status 200, got %d", resp.StatusCode)
+ }
+
+ var tasks []*TaskResponse
+ body, _ := io.ReadAll(resp.Body)
+ if err := json.Unmarshal(body, &tasks); err != nil {
+ t.Fatalf("failed to decode response: %v", err)
+ }
+
+ if len(tasks) != 1 {
+ t.Errorf("expected 1 task, got %d", len(tasks))
+ }
+ if tasks[0].Title != "Test Task" {
+ t.Errorf("expected title 'Test Task', got '%s'", tasks[0].Title)
+ }
+}
+
+func TestCreateTask(t *testing.T) {
+ server, cleanup := setupTestServer(t)
+ defer cleanup()
+
+ // Create task via API
+ createReq := CreateTaskRequest{
+ Title: "New Task",
+ Body: "Task body",
+ Type: "writing",
+ Project: "personal",
+ }
+ reqBody, _ := json.Marshal(createReq)
+
+ req := httptest.NewRequest("POST", "/tasks", bytes.NewReader(reqBody))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+
+ server.handleCreateTask(w, req)
+
+ resp := w.Result()
+ if resp.StatusCode != http.StatusCreated {
+ body, _ := io.ReadAll(resp.Body)
+ t.Fatalf("expected status 201, got %d: %s", resp.StatusCode, string(body))
+ }
+
+ var task TaskResponse
+ body, _ := io.ReadAll(resp.Body)
+ if err := json.Unmarshal(body, &task); err != nil {
+ t.Fatalf("failed to decode response: %v", err)
+ }
+
+ if task.Title != "New Task" {
+ t.Errorf("expected title 'New Task', got '%s'", task.Title)
+ }
+ if task.Status != db.StatusBacklog {
+ t.Errorf("expected status 'backlog', got '%s'", task.Status)
+ }
+}
+
+func TestCreateTaskRequiresTitle(t *testing.T) {
+ server, cleanup := setupTestServer(t)
+ defer cleanup()
+
+ // Create task without title
+ createReq := CreateTaskRequest{
+ Body: "Task body",
+ }
+ reqBody, _ := json.Marshal(createReq)
+
+ req := httptest.NewRequest("POST", "/tasks", bytes.NewReader(reqBody))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+
+ server.handleCreateTask(w, req)
+
+ resp := w.Result()
+ if resp.StatusCode != http.StatusBadRequest {
+ t.Errorf("expected status 400, got %d", resp.StatusCode)
+ }
+}
+
+func TestListProjects(t *testing.T) {
+ server, cleanup := setupTestServer(t)
+ defer cleanup()
+
+ req := httptest.NewRequest("GET", "/projects", nil)
+ w := httptest.NewRecorder()
+
+ server.handleListProjects(w, req)
+
+ resp := w.Result()
+ if resp.StatusCode != http.StatusOK {
+ t.Errorf("expected status 200, got %d", resp.StatusCode)
+ }
+
+ var projects []*ProjectResponse
+ body, _ := io.ReadAll(resp.Body)
+ if err := json.Unmarshal(body, &projects); err != nil {
+ t.Fatalf("failed to decode response: %v", err)
+ }
+
+ // Should have at least the default 'personal' project
+ found := false
+ for _, p := range projects {
+ if p.Name == "personal" {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Error("expected 'personal' project to exist")
+ }
+}
+
+func TestCreateProject(t *testing.T) {
+ server, cleanup := setupTestServer(t)
+ defer cleanup()
+
+ createReq := CreateProjectRequest{
+ Name: "test-project",
+ Path: "/tmp/test-project",
+ Color: "#FF0000",
+ }
+ reqBody, _ := json.Marshal(createReq)
+
+ req := httptest.NewRequest("POST", "/projects", bytes.NewReader(reqBody))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+
+ server.handleCreateProject(w, req)
+
+ resp := w.Result()
+ if resp.StatusCode != http.StatusCreated {
+ body, _ := io.ReadAll(resp.Body)
+ t.Fatalf("expected status 201, got %d: %s", resp.StatusCode, string(body))
+ }
+
+ var project ProjectResponse
+ body, _ := io.ReadAll(resp.Body)
+ if err := json.Unmarshal(body, &project); err != nil {
+ t.Fatalf("failed to decode response: %v", err)
+ }
+
+ if project.Name != "test-project" {
+ t.Errorf("expected name 'test-project', got '%s'", project.Name)
+ }
+ if project.Color != "#FF0000" {
+ t.Errorf("expected color '#FF0000', got '%s'", project.Color)
+ }
+}
+
+func TestGetSettings(t *testing.T) {
+ server, cleanup := setupTestServer(t)
+ defer cleanup()
+
+ // Set a setting first
+ server.db.SetSetting("theme", "dark")
+
+ req := httptest.NewRequest("GET", "/settings", nil)
+ w := httptest.NewRecorder()
+
+ server.handleGetSettings(w, req)
+
+ resp := w.Result()
+ if resp.StatusCode != http.StatusOK {
+ t.Errorf("expected status 200, got %d", resp.StatusCode)
+ }
+
+ var settings map[string]string
+ body, _ := io.ReadAll(resp.Body)
+ if err := json.Unmarshal(body, &settings); err != nil {
+ t.Fatalf("failed to decode response: %v", err)
+ }
+
+ if settings["theme"] != "dark" {
+ t.Errorf("expected theme 'dark', got '%s'", settings["theme"])
+ }
+}
+
+func TestUpdateSettings(t *testing.T) {
+ server, cleanup := setupTestServer(t)
+ defer cleanup()
+
+ settings := map[string]string{
+ "theme": "light",
+ "pane_height": "50",
+ }
+ reqBody, _ := json.Marshal(settings)
+
+ req := httptest.NewRequest("PUT", "/settings", bytes.NewReader(reqBody))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+
+ server.handleUpdateSettings(w, req)
+
+ resp := w.Result()
+ if resp.StatusCode != http.StatusOK {
+ t.Errorf("expected status 200, got %d", resp.StatusCode)
+ }
+
+ // Verify settings were saved
+ theme, _ := server.db.GetSetting("theme")
+ if theme != "light" {
+ t.Errorf("expected theme 'light', got '%s'", theme)
+ }
+
+ paneHeight, _ := server.db.GetSetting("pane_height")
+ if paneHeight != "50" {
+ t.Errorf("expected pane_height '50', got '%s'", paneHeight)
+ }
+}
diff --git a/internal/webapi/tasks.go b/internal/webapi/tasks.go
new file mode 100644
index 00000000..478deaca
--- /dev/null
+++ b/internal/webapi/tasks.go
@@ -0,0 +1,427 @@
+package webapi
+
+import (
+ "net/http"
+ "time"
+
+ "github.com/bborn/workflow/internal/db"
+)
+
+// TaskResponse represents a task in JSON responses.
+type TaskResponse struct {
+ ID int64 `json:"id"`
+ Title string `json:"title"`
+ Body string `json:"body"`
+ Status string `json:"status"`
+ Type string `json:"type"`
+ Project string `json:"project"`
+ WorktreePath string `json:"worktree_path,omitempty"`
+ BranchName string `json:"branch_name,omitempty"`
+ Port int `json:"port,omitempty"`
+ PRUrl string `json:"pr_url,omitempty"`
+ PRNumber int `json:"pr_number,omitempty"`
+ DangerousMode bool `json:"dangerous_mode"`
+ ScheduledAt *time.Time `json:"scheduled_at,omitempty"`
+ Recurrence string `json:"recurrence,omitempty"`
+ LastRunAt *time.Time `json:"last_run_at,omitempty"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+ StartedAt *time.Time `json:"started_at,omitempty"`
+ CompletedAt *time.Time `json:"completed_at,omitempty"`
+}
+
+func taskToResponse(t *db.Task) *TaskResponse {
+ resp := &TaskResponse{
+ ID: t.ID,
+ Title: t.Title,
+ Body: t.Body,
+ Status: t.Status,
+ Type: t.Type,
+ Project: t.Project,
+ WorktreePath: t.WorktreePath,
+ BranchName: t.BranchName,
+ Port: t.Port,
+ PRUrl: t.PRURL,
+ PRNumber: t.PRNumber,
+ DangerousMode: t.DangerousMode,
+ Recurrence: t.Recurrence,
+ CreatedAt: t.CreatedAt.Time,
+ UpdatedAt: t.UpdatedAt.Time,
+ }
+
+ if t.ScheduledAt != nil && !t.ScheduledAt.Time.IsZero() {
+ resp.ScheduledAt = &t.ScheduledAt.Time
+ }
+ if t.LastRunAt != nil && !t.LastRunAt.Time.IsZero() {
+ resp.LastRunAt = &t.LastRunAt.Time
+ }
+ if t.StartedAt != nil && !t.StartedAt.Time.IsZero() {
+ resp.StartedAt = &t.StartedAt.Time
+ }
+ if t.CompletedAt != nil && !t.CompletedAt.Time.IsZero() {
+ resp.CompletedAt = &t.CompletedAt.Time
+ }
+
+ return resp
+}
+
+// handleListTasks handles GET /tasks
+func (s *Server) handleListTasks(w http.ResponseWriter, r *http.Request) {
+ // Parse query parameters
+ opts := db.ListTasksOptions{}
+
+ if status := r.URL.Query().Get("status"); status != "" {
+ opts.Status = status
+ }
+ if project := r.URL.Query().Get("project"); project != "" {
+ opts.Project = project
+ }
+ if taskType := r.URL.Query().Get("type"); taskType != "" {
+ opts.Type = taskType
+ }
+ if r.URL.Query().Get("all") == "true" {
+ opts.IncludeClosed = true
+ }
+
+ tasks, err := s.db.ListTasks(opts)
+ if err != nil {
+ s.logger.Error("list tasks failed", "error", err)
+ jsonError(w, "Failed to list tasks", http.StatusInternalServerError)
+ return
+ }
+
+ responses := make([]*TaskResponse, len(tasks))
+ for i, t := range tasks {
+ responses[i] = taskToResponse(t)
+ }
+
+ jsonResponse(w, responses, http.StatusOK)
+}
+
+// CreateTaskRequest represents a request to create a task.
+type CreateTaskRequest struct {
+ Title string `json:"title"`
+ Body string `json:"body"`
+ Type string `json:"type"`
+ Project string `json:"project"`
+ ScheduledAt string `json:"scheduled_at,omitempty"`
+ Recurrence string `json:"recurrence,omitempty"`
+}
+
+// handleCreateTask handles POST /tasks
+func (s *Server) handleCreateTask(w http.ResponseWriter, r *http.Request) {
+ var req CreateTaskRequest
+ if err := parseJSON(r, &req); err != nil {
+ jsonError(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ if req.Title == "" {
+ jsonError(w, "Title is required", http.StatusBadRequest)
+ return
+ }
+
+ // Set defaults
+ if req.Project == "" {
+ req.Project = "personal"
+ }
+ if req.Type == "" {
+ req.Type = "code"
+ }
+
+ // Build task struct
+ task := &db.Task{
+ Title: req.Title,
+ Body: req.Body,
+ Type: req.Type,
+ Project: req.Project,
+ Status: db.StatusBacklog,
+ Recurrence: req.Recurrence,
+ }
+
+ // Handle scheduling
+ if req.ScheduledAt != "" {
+ scheduledAt, err := time.Parse(time.RFC3339, req.ScheduledAt)
+ if err == nil {
+ task.ScheduledAt = &db.LocalTime{Time: scheduledAt}
+ }
+ }
+
+ if err := s.db.CreateTask(task); err != nil {
+ s.logger.Error("create task failed", "error", err)
+ jsonError(w, "Failed to create task", http.StatusInternalServerError)
+ return
+ }
+
+ // Reload task to get all fields
+ task, _ = s.db.GetTask(task.ID)
+
+ // Broadcast update
+ s.BroadcastTaskUpdate(task)
+
+ jsonResponse(w, taskToResponse(task), http.StatusCreated)
+}
+
+// handleGetTask handles GET /tasks/{id}
+func (s *Server) handleGetTask(w http.ResponseWriter, r *http.Request) {
+ id, err := getIDParam(r)
+ if err != nil {
+ jsonError(w, "Invalid task ID", http.StatusBadRequest)
+ return
+ }
+
+ task, err := s.db.GetTask(id)
+ if err != nil {
+ s.logger.Error("get task failed", "error", err)
+ jsonError(w, "Failed to get task", http.StatusInternalServerError)
+ return
+ }
+
+ if task == nil {
+ jsonError(w, "Task not found", http.StatusNotFound)
+ return
+ }
+
+ jsonResponse(w, taskToResponse(task), http.StatusOK)
+}
+
+// UpdateTaskRequest represents a request to update a task.
+type UpdateTaskRequest struct {
+ Title *string `json:"title,omitempty"`
+ Body *string `json:"body,omitempty"`
+ Status *string `json:"status,omitempty"`
+ Type *string `json:"type,omitempty"`
+ Project *string `json:"project,omitempty"`
+ ScheduledAt *string `json:"scheduled_at,omitempty"`
+ Recurrence *string `json:"recurrence,omitempty"`
+}
+
+// handleUpdateTask handles PUT /tasks/{id}
+func (s *Server) handleUpdateTask(w http.ResponseWriter, r *http.Request) {
+ id, err := getIDParam(r)
+ if err != nil {
+ jsonError(w, "Invalid task ID", http.StatusBadRequest)
+ return
+ }
+
+ var req UpdateTaskRequest
+ if err := parseJSON(r, &req); err != nil {
+ jsonError(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ task, err := s.db.GetTask(id)
+ if err != nil || task == nil {
+ jsonError(w, "Task not found", http.StatusNotFound)
+ return
+ }
+
+ // Apply updates
+ if req.Title != nil {
+ task.Title = *req.Title
+ }
+ if req.Body != nil {
+ task.Body = *req.Body
+ }
+ if req.Status != nil {
+ task.Status = *req.Status
+ }
+ if req.Type != nil {
+ task.Type = *req.Type
+ }
+ if req.Project != nil {
+ task.Project = *req.Project
+ }
+
+ if err := s.db.UpdateTask(task); err != nil {
+ s.logger.Error("update task failed", "error", err)
+ jsonError(w, "Failed to update task", http.StatusInternalServerError)
+ return
+ }
+
+ // Handle scheduling
+ if req.ScheduledAt != nil || req.Recurrence != nil {
+ var scheduledAt *db.LocalTime
+ var recurrence string
+
+ if req.ScheduledAt != nil {
+ if *req.ScheduledAt != "" {
+ t, err := time.Parse(time.RFC3339, *req.ScheduledAt)
+ if err == nil {
+ scheduledAt = &db.LocalTime{Time: t}
+ }
+ }
+ }
+ if req.Recurrence != nil {
+ recurrence = *req.Recurrence
+ }
+
+ s.db.UpdateTaskSchedule(id, scheduledAt, recurrence, nil)
+ }
+
+ // Reload task
+ task, _ = s.db.GetTask(id)
+
+ // Broadcast update
+ s.BroadcastTaskUpdate(task)
+
+ jsonResponse(w, taskToResponse(task), http.StatusOK)
+}
+
+// handleDeleteTask handles DELETE /tasks/{id}
+func (s *Server) handleDeleteTask(w http.ResponseWriter, r *http.Request) {
+ id, err := getIDParam(r)
+ if err != nil {
+ jsonError(w, "Invalid task ID", http.StatusBadRequest)
+ return
+ }
+
+ if err := s.db.DeleteTask(id); err != nil {
+ s.logger.Error("delete task failed", "error", err)
+ jsonError(w, "Failed to delete task", http.StatusInternalServerError)
+ return
+ }
+
+ // Broadcast deletion
+ s.wsHub.Broadcast(Message{
+ Type: "task_deleted",
+ Data: map[string]int64{"id": id},
+ })
+
+ w.WriteHeader(http.StatusNoContent)
+}
+
+// handleQueueTask handles POST /tasks/{id}/queue
+func (s *Server) handleQueueTask(w http.ResponseWriter, r *http.Request) {
+ id, err := getIDParam(r)
+ if err != nil {
+ jsonError(w, "Invalid task ID", http.StatusBadRequest)
+ return
+ }
+
+ task, err := s.db.GetTask(id)
+ if err != nil || task == nil {
+ jsonError(w, "Task not found", http.StatusNotFound)
+ return
+ }
+
+ // Update status to queued
+ if err := s.db.UpdateTaskStatus(id, "queued"); err != nil {
+ s.logger.Error("queue task failed", "error", err)
+ jsonError(w, "Failed to queue task", http.StatusInternalServerError)
+ return
+ }
+
+ // Reload task
+ task, _ = s.db.GetTask(id)
+
+ // Broadcast update
+ s.BroadcastTaskUpdate(task)
+
+ jsonResponse(w, taskToResponse(task), http.StatusOK)
+}
+
+// RetryTaskRequest represents a request to retry a task.
+type RetryTaskRequest struct {
+ Feedback string `json:"feedback,omitempty"`
+}
+
+// handleRetryTask handles POST /tasks/{id}/retry
+func (s *Server) handleRetryTask(w http.ResponseWriter, r *http.Request) {
+ id, err := getIDParam(r)
+ if err != nil {
+ jsonError(w, "Invalid task ID", http.StatusBadRequest)
+ return
+ }
+
+ var req RetryTaskRequest
+ parseJSON(r, &req) // Feedback is optional
+
+ task, err := s.db.GetTask(id)
+ if err != nil || task == nil {
+ jsonError(w, "Task not found", http.StatusNotFound)
+ return
+ }
+
+ // If feedback provided, add to body
+ if req.Feedback != "" {
+ newBody := task.Body
+ if newBody != "" {
+ newBody += "\n\n---\nFeedback:\n"
+ }
+ newBody += req.Feedback
+ task.Body = newBody
+ s.db.UpdateTask(task)
+ }
+
+ // Update status to queued
+ if err := s.db.UpdateTaskStatus(id, "queued"); err != nil {
+ s.logger.Error("retry task failed", "error", err)
+ jsonError(w, "Failed to retry task", http.StatusInternalServerError)
+ return
+ }
+
+ // Reload task
+ task, _ = s.db.GetTask(id)
+
+ // Broadcast update
+ s.BroadcastTaskUpdate(task)
+
+ jsonResponse(w, taskToResponse(task), http.StatusOK)
+}
+
+// handleCloseTask handles POST /tasks/{id}/close
+func (s *Server) handleCloseTask(w http.ResponseWriter, r *http.Request) {
+ id, err := getIDParam(r)
+ if err != nil {
+ jsonError(w, "Invalid task ID", http.StatusBadRequest)
+ return
+ }
+
+ task, err := s.db.GetTask(id)
+ if err != nil || task == nil {
+ jsonError(w, "Task not found", http.StatusNotFound)
+ return
+ }
+
+ // Update status to done
+ if err := s.db.UpdateTaskStatus(id, "done"); err != nil {
+ s.logger.Error("close task failed", "error", err)
+ jsonError(w, "Failed to close task", http.StatusInternalServerError)
+ return
+ }
+
+ // Reload task
+ task, _ = s.db.GetTask(id)
+
+ // Broadcast update
+ s.BroadcastTaskUpdate(task)
+
+ jsonResponse(w, taskToResponse(task), http.StatusOK)
+}
+
+// handleGetTaskLogs handles GET /tasks/{id}/logs
+func (s *Server) handleGetTaskLogs(w http.ResponseWriter, r *http.Request) {
+ id, err := getIDParam(r)
+ if err != nil {
+ jsonError(w, "Invalid task ID", http.StatusBadRequest)
+ return
+ }
+
+ // Parse limit parameter
+ limit := 100
+ if l := r.URL.Query().Get("limit"); l != "" {
+ if parsed, err := getIDParam(r); err == nil && parsed > 0 {
+ limit = int(parsed)
+ }
+ }
+
+ logs, err := s.db.GetTaskLogs(id, limit)
+ if err != nil {
+ s.logger.Error("get task logs failed", "error", err)
+ jsonError(w, "Failed to get task logs", http.StatusInternalServerError)
+ return
+ }
+
+ jsonResponse(w, logs, http.StatusOK)
+}
diff --git a/internal/webapi/terminal.go b/internal/webapi/terminal.go
new file mode 100644
index 00000000..525ed3e2
--- /dev/null
+++ b/internal/webapi/terminal.go
@@ -0,0 +1,178 @@
+package webapi
+
+import (
+ "fmt"
+ "net/http"
+ "os/exec"
+ "strings"
+ "sync"
+)
+
+// terminalManager manages ttyd processes for task terminals.
+type terminalManager struct {
+ mu sync.Mutex
+ sessions map[int64]*ttydSession
+}
+
+type ttydSession struct {
+ taskID int64
+ port int
+ process *exec.Cmd
+}
+
+var terminals = &terminalManager{
+ sessions: make(map[int64]*ttydSession),
+}
+
+// BasePort is the starting port for ttyd instances.
+const ttydBasePort = 7681
+
+// getTerminalPort returns the port for a task's terminal.
+func getTerminalPort(taskID int64) int {
+ return ttydBasePort + int(taskID%1000)
+}
+
+// findDaemonSession finds the current task-daemon session.
+func findDaemonSession() string {
+ out, err := exec.Command("tmux", "list-sessions", "-F", "#{session_name}").Output()
+ if err != nil {
+ return ""
+ }
+ for _, line := range strings.Split(string(out), "\n") {
+ if strings.HasPrefix(line, "task-daemon-") {
+ return line
+ }
+ }
+ return ""
+}
+
+// handleGetTerminal returns terminal connection info for a task.
+func (s *Server) handleGetTerminal(w http.ResponseWriter, r *http.Request) {
+ taskID, err := getIDParam(r)
+ if err != nil {
+ jsonError(w, "invalid task ID", http.StatusBadRequest)
+ return
+ }
+
+ // Get task to find its daemon session
+ task, err := s.db.GetTask(taskID)
+ if err != nil {
+ jsonError(w, "task not found", http.StatusNotFound)
+ return
+ }
+
+ // Try to find the daemon session - use task's recorded session or auto-detect
+ daemonSession := task.DaemonSession
+ if daemonSession == "" {
+ daemonSession = findDaemonSession()
+ }
+ if daemonSession == "" {
+ jsonError(w, "task has no active terminal session", http.StatusNotFound)
+ return
+ }
+
+ windowName := fmt.Sprintf("task-%d", taskID)
+ tmuxTarget := fmt.Sprintf("%s:%s", daemonSession, windowName)
+
+ // Check if tmux window exists
+ if err := exec.Command("tmux", "has-session", "-t", tmuxTarget).Run(); err != nil {
+ jsonError(w, "task terminal session not running", http.StatusNotFound)
+ return
+ }
+
+ // Start ttyd if not already running
+ port, err := terminals.ensureRunning(taskID, tmuxTarget)
+ if err != nil {
+ s.logger.Error("failed to start terminal", "task", taskID, "error", err)
+ jsonError(w, "failed to start terminal: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ jsonResponse(w, map[string]interface{}{
+ "task_id": taskID,
+ "port": port,
+ "tmux_target": tmuxTarget,
+ "websocket_url": fmt.Sprintf("ws://localhost:%d/ws", port),
+ }, http.StatusOK)
+}
+
+// ensureRunning starts ttyd for a task if not already running.
+func (tm *terminalManager) ensureRunning(taskID int64, tmuxTarget string) (int, error) {
+ tm.mu.Lock()
+ defer tm.mu.Unlock()
+
+ // Check if already running
+ if session, ok := tm.sessions[taskID]; ok {
+ // Verify process is still alive
+ if session.process != nil && session.process.Process != nil {
+ if err := session.process.Process.Signal(nil); err == nil {
+ return session.port, nil
+ }
+ }
+ // Process died, clean up
+ delete(tm.sessions, taskID)
+ }
+
+ port := getTerminalPort(taskID)
+
+ // Check if ttyd is available
+ if _, err := exec.LookPath("ttyd"); err != nil {
+ return 0, fmt.Errorf("ttyd not installed: %w", err)
+ }
+
+ // Start ttyd attached to the tmux session
+ // -W = writable (interactive)
+ // -p = port
+ // Use -- to separate ttyd options from the command
+ cmd := exec.Command("ttyd",
+ "-W",
+ "-p", fmt.Sprintf("%d", port),
+ "--",
+ "tmux", "attach-session", "-t", tmuxTarget,
+ )
+
+ if err := cmd.Start(); err != nil {
+ return 0, fmt.Errorf("failed to start ttyd: %w", err)
+ }
+
+ tm.sessions[taskID] = &ttydSession{
+ taskID: taskID,
+ port: port,
+ process: cmd,
+ }
+
+ // Clean up process when it exits
+ go func() {
+ cmd.Wait()
+ tm.mu.Lock()
+ delete(tm.sessions, taskID)
+ tm.mu.Unlock()
+ }()
+
+ return port, nil
+}
+
+// stopTerminal stops the ttyd process for a task.
+func (tm *terminalManager) stopTerminal(taskID int64) {
+ tm.mu.Lock()
+ defer tm.mu.Unlock()
+
+ if session, ok := tm.sessions[taskID]; ok {
+ if session.process != nil && session.process.Process != nil {
+ session.process.Process.Kill()
+ }
+ delete(tm.sessions, taskID)
+ }
+}
+
+// handleStopTerminal stops a task's terminal.
+func (s *Server) handleStopTerminal(w http.ResponseWriter, r *http.Request) {
+ taskID, err := getIDParam(r)
+ if err != nil {
+ jsonError(w, "invalid task ID", http.StatusBadRequest)
+ return
+ }
+
+ terminals.stopTerminal(taskID)
+ w.WriteHeader(http.StatusNoContent)
+}
diff --git a/internal/webserver/handlers.go b/internal/webserver/handlers.go
new file mode 100644
index 00000000..1c369f1d
--- /dev/null
+++ b/internal/webserver/handlers.go
@@ -0,0 +1,500 @@
+package webserver
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/bborn/workflow/internal/auth"
+)
+
+// handleGoogleAuth initiates Google OAuth flow.
+func (s *Server) handleGoogleAuth(w http.ResponseWriter, r *http.Request) {
+ s.handleOAuthStart(w, r, auth.ProviderGoogle)
+}
+
+// handleGitHubAuth initiates GitHub OAuth flow.
+func (s *Server) handleGitHubAuth(w http.ResponseWriter, r *http.Request) {
+ s.handleOAuthStart(w, r, auth.ProviderGitHub)
+}
+
+// handleOAuthStart initiates an OAuth flow for the given provider.
+func (s *Server) handleOAuthStart(w http.ResponseWriter, r *http.Request, provider auth.Provider) {
+ if !auth.IsConfigured(provider) {
+ jsonError(w, fmt.Sprintf("%s OAuth not configured", provider), http.StatusServiceUnavailable)
+ return
+ }
+
+ // Generate state
+ state, err := auth.GenerateOAuthState()
+ if err != nil {
+ s.logger.Error("generate oauth state failed", "error", err)
+ jsonError(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // Store state in cookie with provider info
+ stateWithProvider := fmt.Sprintf("%s:%s", provider, state)
+ s.sessionMgr.SetOAuthStateCookie(w, stateWithProvider)
+
+ // Get OAuth config
+ redirectURL := fmt.Sprintf("%s/api/auth/callback", s.baseURL)
+ config := auth.GetConfig(provider, redirectURL)
+
+ // Redirect to provider
+ url := config.OAuth2.AuthCodeURL(state)
+ http.Redirect(w, r, url, http.StatusTemporaryRedirect)
+}
+
+// handleOAuthCallback handles the OAuth callback from the provider.
+func (s *Server) handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
+ // Get state from cookie
+ stateWithProvider := s.sessionMgr.GetOAuthStateCookie(w, r)
+ if stateWithProvider == "" {
+ jsonError(w, "Invalid OAuth state", http.StatusBadRequest)
+ return
+ }
+
+ // Parse provider from state
+ var provider auth.Provider
+ var expectedState string
+ if n, _ := fmt.Sscanf(stateWithProvider, "%s:%s", &provider, &expectedState); n != 2 {
+ // Fallback to parsing with string operations
+ for _, p := range []auth.Provider{auth.ProviderGoogle, auth.ProviderGitHub} {
+ prefix := string(p) + ":"
+ if len(stateWithProvider) > len(prefix) && stateWithProvider[:len(prefix)] == prefix {
+ provider = p
+ expectedState = stateWithProvider[len(prefix):]
+ break
+ }
+ }
+ }
+
+ if provider == "" {
+ jsonError(w, "Invalid OAuth state format", http.StatusBadRequest)
+ return
+ }
+
+ // Verify state
+ state := r.URL.Query().Get("state")
+ if state != expectedState {
+ jsonError(w, "OAuth state mismatch", http.StatusBadRequest)
+ return
+ }
+
+ // Check for error from provider
+ if errMsg := r.URL.Query().Get("error"); errMsg != "" {
+ errDesc := r.URL.Query().Get("error_description")
+ s.logger.Error("oauth error", "provider", provider, "error", errMsg, "description", errDesc)
+ http.Redirect(w, r, fmt.Sprintf("/?error=%s", errMsg), http.StatusTemporaryRedirect)
+ return
+ }
+
+ // Exchange code for tokens
+ code := r.URL.Query().Get("code")
+ if code == "" {
+ jsonError(w, "Missing authorization code", http.StatusBadRequest)
+ return
+ }
+
+ redirectURL := fmt.Sprintf("%s/api/auth/callback", s.baseURL)
+ config := auth.GetConfig(provider, redirectURL)
+
+ token, err := auth.ExchangeCode(r.Context(), config, code)
+ if err != nil {
+ s.logger.Error("exchange code failed", "error", err)
+ jsonError(w, "Failed to exchange authorization code", http.StatusInternalServerError)
+ return
+ }
+
+ // Get user info from provider
+ userInfo, err := auth.GetUserInfo(r.Context(), config, token)
+ if err != nil {
+ s.logger.Error("get user info failed", "error", err)
+ jsonError(w, "Failed to get user information", http.StatusInternalServerError)
+ return
+ }
+
+ // Get or create user
+ expiresAt := auth.TokenExpiry(token)
+ user, isNew, err := s.db.GetOrCreateUserByOAuth(
+ string(provider),
+ userInfo.ProviderAccountID,
+ userInfo.Email,
+ userInfo.Name,
+ userInfo.AvatarURL,
+ token.AccessToken,
+ token.RefreshToken,
+ expiresAt,
+ )
+ if err != nil {
+ s.logger.Error("get or create user failed", "error", err)
+ jsonError(w, "Failed to create user", http.StatusInternalServerError)
+ return
+ }
+
+ s.logger.Info("user authenticated",
+ "user_id", user.ID,
+ "email", user.Email,
+ "provider", provider,
+ "new_user", isNew,
+ )
+
+ // Create session
+ if _, err := s.sessionMgr.CreateSession(w, user.ID); err != nil {
+ s.logger.Error("create session failed", "error", err)
+ jsonError(w, "Failed to create session", http.StatusInternalServerError)
+ return
+ }
+
+ // Provision sprite for new users
+ if isNew && s.spriteMgr != nil {
+ go func() {
+ if _, err := s.spriteMgr.ProvisionSprite(r.Context(), user.ID); err != nil {
+ s.logger.Error("provision sprite failed", "user_id", user.ID, "error", err)
+ }
+ }()
+ }
+
+ // Redirect to dashboard
+ http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
+}
+
+// handleLogout logs the user out.
+func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
+ if err := s.sessionMgr.DeleteSession(w, r); err != nil {
+ s.logger.Error("delete session failed", "error", err)
+ }
+ jsonResponse(w, map[string]bool{"success": true}, http.StatusOK)
+}
+
+// handleGetMe returns the current user's information.
+func (s *Server) handleGetMe(w http.ResponseWriter, r *http.Request) {
+ _, user, err := s.sessionMgr.GetSessionWithUser(r)
+ if err != nil {
+ s.logger.Error("get session failed", "error", err)
+ jsonError(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ if user == nil {
+ jsonError(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ // Get user's sprite status
+ var spriteStatus interface{}
+ if s.spriteMgr != nil {
+ sprite, err := s.spriteMgr.GetUserSprite(r.Context(), user.ID)
+ if err == nil && sprite != nil {
+ spriteStatus = map[string]interface{}{
+ "id": sprite.ID,
+ "name": sprite.SpriteName,
+ "status": sprite.Status,
+ "region": sprite.Region,
+ "created_at": sprite.CreatedAt,
+ }
+ }
+ }
+
+ jsonResponse(w, map[string]interface{}{
+ "id": user.ID,
+ "email": user.Email,
+ "name": user.Name,
+ "avatar_url": user.AvatarURL,
+ "sprite": spriteStatus,
+ }, http.StatusOK)
+}
+
+// handleGetSprite returns the user's sprite status.
+func (s *Server) handleGetSprite(w http.ResponseWriter, r *http.Request) {
+ user := getUserFromContext(r)
+ if user == nil {
+ jsonError(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ if s.spriteMgr == nil {
+ jsonError(w, "Sprites not configured", http.StatusServiceUnavailable)
+ return
+ }
+
+ sprite, err := s.spriteMgr.GetUserSprite(r.Context(), user.ID)
+ if err != nil {
+ s.logger.Error("get user sprite failed", "error", err)
+ jsonError(w, "Failed to get sprite", http.StatusInternalServerError)
+ return
+ }
+
+ if sprite == nil {
+ jsonResponse(w, map[string]interface{}{"sprite": nil}, http.StatusOK)
+ return
+ }
+
+ // Get current status from Fly
+ status, _ := s.spriteMgr.GetSpriteStatus(r.Context(), sprite)
+
+ jsonResponse(w, map[string]interface{}{
+ "id": sprite.ID,
+ "name": sprite.SpriteName,
+ "status": status,
+ "region": sprite.Region,
+ "created_at": sprite.CreatedAt,
+ }, http.StatusOK)
+}
+
+// handleCreateSprite creates a sprite for the user.
+func (s *Server) handleCreateSprite(w http.ResponseWriter, r *http.Request) {
+ user := getUserFromContext(r)
+ if user == nil {
+ jsonError(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ if s.spriteMgr == nil {
+ jsonError(w, "Sprites not configured", http.StatusServiceUnavailable)
+ return
+ }
+
+ sprite, err := s.spriteMgr.ProvisionSprite(r.Context(), user.ID)
+ if err != nil {
+ s.logger.Error("provision sprite failed", "error", err)
+ jsonError(w, fmt.Sprintf("Failed to create sprite: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ jsonResponse(w, map[string]interface{}{
+ "id": sprite.ID,
+ "name": sprite.SpriteName,
+ "status": sprite.Status,
+ "region": sprite.Region,
+ "created_at": sprite.CreatedAt,
+ }, http.StatusCreated)
+}
+
+// handleStartSprite starts the user's sprite.
+func (s *Server) handleStartSprite(w http.ResponseWriter, r *http.Request) {
+ user := getUserFromContext(r)
+ if user == nil {
+ jsonError(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ if s.spriteMgr == nil {
+ jsonError(w, "Sprites not configured", http.StatusServiceUnavailable)
+ return
+ }
+
+ sprite, err := s.spriteMgr.GetUserSprite(r.Context(), user.ID)
+ if err != nil {
+ jsonError(w, "Failed to get sprite", http.StatusInternalServerError)
+ return
+ }
+
+ if sprite == nil {
+ jsonError(w, "No sprite found", http.StatusNotFound)
+ return
+ }
+
+ if err := s.spriteMgr.StartSprite(r.Context(), sprite); err != nil {
+ s.logger.Error("start sprite failed", "error", err)
+ jsonError(w, fmt.Sprintf("Failed to start sprite: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ jsonResponse(w, map[string]bool{"success": true}, http.StatusOK)
+}
+
+// handleStopSprite stops the user's sprite.
+func (s *Server) handleStopSprite(w http.ResponseWriter, r *http.Request) {
+ user := getUserFromContext(r)
+ if user == nil {
+ jsonError(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ if s.spriteMgr == nil {
+ jsonError(w, "Sprites not configured", http.StatusServiceUnavailable)
+ return
+ }
+
+ sprite, err := s.spriteMgr.GetUserSprite(r.Context(), user.ID)
+ if err != nil {
+ jsonError(w, "Failed to get sprite", http.StatusInternalServerError)
+ return
+ }
+
+ if sprite == nil {
+ jsonError(w, "No sprite found", http.StatusNotFound)
+ return
+ }
+
+ if err := s.spriteMgr.StopSprite(r.Context(), sprite); err != nil {
+ s.logger.Error("stop sprite failed", "error", err)
+ jsonError(w, fmt.Sprintf("Failed to stop sprite: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ jsonResponse(w, map[string]bool{"success": true}, http.StatusOK)
+}
+
+// handleDestroySprite destroys the user's sprite.
+func (s *Server) handleDestroySprite(w http.ResponseWriter, r *http.Request) {
+ user := getUserFromContext(r)
+ if user == nil {
+ jsonError(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ if s.spriteMgr == nil {
+ jsonError(w, "Sprites not configured", http.StatusServiceUnavailable)
+ return
+ }
+
+ sprite, err := s.spriteMgr.GetUserSprite(r.Context(), user.ID)
+ if err != nil {
+ jsonError(w, "Failed to get sprite", http.StatusInternalServerError)
+ return
+ }
+
+ if sprite == nil {
+ jsonError(w, "No sprite found", http.StatusNotFound)
+ return
+ }
+
+ if err := s.spriteMgr.DestroySprite(r.Context(), sprite); err != nil {
+ s.logger.Error("destroy sprite failed", "error", err)
+ jsonError(w, fmt.Sprintf("Failed to destroy sprite: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ jsonResponse(w, map[string]bool{"success": true}, http.StatusOK)
+}
+
+// handleGetSSHConfig returns SSH config for connecting to the user's sprite.
+func (s *Server) handleGetSSHConfig(w http.ResponseWriter, r *http.Request) {
+ user := getUserFromContext(r)
+ if user == nil {
+ jsonError(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ if s.spriteMgr == nil {
+ jsonError(w, "Sprites not configured", http.StatusServiceUnavailable)
+ return
+ }
+
+ sprite, err := s.spriteMgr.GetUserSprite(r.Context(), user.ID)
+ if err != nil {
+ jsonError(w, "Failed to get sprite", http.StatusInternalServerError)
+ return
+ }
+
+ if sprite == nil {
+ jsonError(w, "No sprite found", http.StatusNotFound)
+ return
+ }
+
+ config, err := s.spriteMgr.GetSSHConfig(r.Context(), sprite)
+ if err != nil {
+ jsonError(w, "Failed to get SSH config", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/plain")
+ w.Write([]byte(config))
+}
+
+// handleExport exports the user's task database.
+func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) {
+ user := getUserFromContext(r)
+ if user == nil {
+ jsonError(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ if s.spriteMgr == nil {
+ jsonError(w, "Sprites not configured", http.StatusServiceUnavailable)
+ return
+ }
+
+ sprite, err := s.spriteMgr.GetUserSprite(r.Context(), user.ID)
+ if err != nil {
+ jsonError(w, "Failed to get sprite", http.StatusInternalServerError)
+ return
+ }
+
+ if sprite == nil {
+ jsonError(w, "No sprite found", http.StatusNotFound)
+ return
+ }
+
+ data, err := s.spriteMgr.ExportDatabase(r.Context(), sprite)
+ if err != nil {
+ s.logger.Error("export database failed", "error", err)
+ jsonError(w, "Failed to export database", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/octet-stream")
+ w.Header().Set("Content-Disposition", "attachment; filename=tasks-export.db")
+ w.Write(data)
+}
+
+// handleProxyTasks proxies requests to the user's sprite.
+func (s *Server) handleProxyTasks(w http.ResponseWriter, r *http.Request) {
+ user := getUserFromContext(r)
+ if user == nil {
+ jsonError(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ if s.spriteProxy == nil {
+ jsonError(w, "Sprites not configured", http.StatusServiceUnavailable)
+ return
+ }
+
+ sprite, err := s.spriteMgr.GetUserSprite(r.Context(), user.ID)
+ if err != nil {
+ jsonError(w, "Failed to get sprite", http.StatusInternalServerError)
+ return
+ }
+
+ if sprite == nil {
+ jsonError(w, "No sprite found. Please create a sprite first.", http.StatusNotFound)
+ return
+ }
+
+ if err := s.spriteProxy.ProxyRequest(r.Context(), sprite, w, r); err != nil {
+ s.logger.Error("proxy request failed", "error", err)
+ jsonError(w, "Sprite unavailable", http.StatusBadGateway)
+ }
+}
+
+// handleWebSocket handles WebSocket connections.
+func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
+ user := getUserFromContext(r)
+ if user == nil {
+ jsonError(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ if s.spriteProxy == nil {
+ jsonError(w, "Sprites not configured", http.StatusServiceUnavailable)
+ return
+ }
+
+ sprite, err := s.spriteMgr.GetUserSprite(r.Context(), user.ID)
+ if err != nil {
+ jsonError(w, "Failed to get sprite", http.StatusInternalServerError)
+ return
+ }
+
+ if sprite == nil {
+ jsonError(w, "No sprite found", http.StatusNotFound)
+ return
+ }
+
+ if err := s.spriteProxy.ProxyWebSocket(r.Context(), sprite, w, r); err != nil {
+ s.logger.Error("websocket proxy failed", "error", err)
+ }
+}
diff --git a/internal/webserver/middleware.go b/internal/webserver/middleware.go
new file mode 100644
index 00000000..64d5eae8
--- /dev/null
+++ b/internal/webserver/middleware.go
@@ -0,0 +1,133 @@
+package webserver
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "time"
+
+ "github.com/bborn/workflow/internal/hostdb"
+)
+
+// contextKey is a type for context keys.
+type contextKey string
+
+const (
+ userContextKey contextKey = "user"
+ sessionContextKey contextKey = "session"
+)
+
+// loggingMiddleware logs HTTP requests.
+func (s *Server) loggingMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+
+ // Wrap response writer to capture status code
+ wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
+
+ next.ServeHTTP(wrapped, r)
+
+ s.logger.Info("request",
+ "method", r.Method,
+ "path", r.URL.Path,
+ "status", wrapped.statusCode,
+ "duration", time.Since(start),
+ "ip", r.RemoteAddr,
+ )
+ })
+}
+
+// responseWriter wraps http.ResponseWriter to capture the status code.
+type responseWriter struct {
+ http.ResponseWriter
+ statusCode int
+}
+
+func (rw *responseWriter) WriteHeader(code int) {
+ rw.statusCode = code
+ rw.ResponseWriter.WriteHeader(code)
+}
+
+// corsMiddleware adds CORS headers.
+func (s *Server) corsMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Allow requests from the frontend origin
+ origin := r.Header.Get("Origin")
+ if origin == "" {
+ origin = s.baseURL
+ }
+
+ // Only allow specific origins in production
+ allowedOrigins := []string{s.baseURL, "http://localhost:5173", "http://localhost:3000"}
+ allowed := false
+ for _, o := range allowedOrigins {
+ if o == origin {
+ allowed = true
+ break
+ }
+ }
+
+ if allowed {
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
+ w.Header().Set("Access-Control-Max-Age", "86400")
+ }
+
+ // Handle preflight requests
+ if r.Method == "OPTIONS" {
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+
+ next.ServeHTTP(w, r)
+ })
+}
+
+// requireAuth is middleware that requires authentication.
+func (s *Server) requireAuth(handler http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ session, user, err := s.sessionMgr.GetSessionWithUser(r)
+ if err != nil {
+ s.logger.Error("get session failed", "error", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ if session == nil || user == nil {
+ jsonError(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ // Add user and session to context
+ ctx := context.WithValue(r.Context(), userContextKey, user)
+ ctx = context.WithValue(ctx, sessionContextKey, session)
+
+ handler(w, r.WithContext(ctx))
+ }
+}
+
+// getUserFromContext retrieves the user from the request context.
+func getUserFromContext(r *http.Request) *hostdb.User {
+ user, ok := r.Context().Value(userContextKey).(*hostdb.User)
+ if !ok {
+ return nil
+ }
+ return user
+}
+
+// jsonResponse writes a JSON response.
+func jsonResponse(w http.ResponseWriter, data interface{}, status int) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+ json.NewEncoder(w).Encode(data)
+}
+
+// jsonError writes a JSON error response.
+func jsonError(w http.ResponseWriter, message string, status int) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+ json.NewEncoder(w).Encode(map[string]string{"error": message})
+}
+
diff --git a/internal/webserver/server.go b/internal/webserver/server.go
new file mode 100644
index 00000000..f237fc0f
--- /dev/null
+++ b/internal/webserver/server.go
@@ -0,0 +1,243 @@
+// Package webserver provides the HTTP server for the web UI.
+package webserver
+
+import (
+ "context"
+ "io"
+ "io/fs"
+ "net/http"
+ "os"
+ "path"
+ "strings"
+ "time"
+
+ "github.com/bborn/workflow/internal/auth"
+ "github.com/bborn/workflow/internal/hostdb"
+ "github.com/bborn/workflow/internal/sprite"
+ "github.com/charmbracelet/log"
+)
+
+// Server is the web server for taskyou.
+type Server struct {
+ addr string
+ db *hostdb.DB
+ sessionMgr *auth.SessionManager
+ spriteClient *sprite.Client
+ spriteMgr *sprite.Manager
+ spriteProxy *sprite.ProxyHandler
+ baseURL string
+ staticFS fs.FS
+ logger *log.Logger
+}
+
+// Config holds server configuration.
+type Config struct {
+ Addr string
+ DB *hostdb.DB
+ BaseURL string
+ Secure bool // Use secure cookies
+ Domain string // Cookie domain
+ StaticFS fs.FS // Embedded static files (optional)
+}
+
+// New creates a new server.
+func New(cfg Config) (*Server, error) {
+ sessionMgr := auth.NewSessionManager(cfg.DB, cfg.Secure, cfg.Domain)
+
+ spriteClient, err := sprite.NewClient()
+ if err != nil {
+ // Sprites client is optional for development
+ log.Warn("sprites client not configured", "error", err)
+ }
+
+ var spriteMgr *sprite.Manager
+ var spriteProxy *sprite.ProxyHandler
+ if spriteClient != nil {
+ spriteMgr = sprite.NewManager(spriteClient, cfg.DB)
+ spriteProxy = sprite.NewProxyHandler(spriteMgr)
+ }
+
+ return &Server{
+ addr: cfg.Addr,
+ db: cfg.DB,
+ sessionMgr: sessionMgr,
+ spriteClient: spriteClient,
+ spriteMgr: spriteMgr,
+ spriteProxy: spriteProxy,
+ baseURL: cfg.BaseURL,
+ staticFS: cfg.StaticFS,
+ logger: log.NewWithOptions(os.Stderr, log.Options{Prefix: "webserver"}),
+ }, nil
+}
+
+// Start starts the server.
+func (s *Server) Start(ctx context.Context) error {
+ mux := http.NewServeMux()
+
+ // Auth routes
+ mux.HandleFunc("GET /api/auth/google", s.handleGoogleAuth)
+ mux.HandleFunc("GET /api/auth/github", s.handleGitHubAuth)
+ mux.HandleFunc("GET /api/auth/callback", s.handleOAuthCallback)
+ mux.HandleFunc("POST /api/auth/logout", s.handleLogout)
+ mux.HandleFunc("GET /api/auth/me", s.handleGetMe)
+
+ // Sprite management routes
+ mux.HandleFunc("GET /api/sprite", s.requireAuth(s.handleGetSprite))
+ mux.HandleFunc("POST /api/sprite", s.requireAuth(s.handleCreateSprite))
+ mux.HandleFunc("POST /api/sprite/start", s.requireAuth(s.handleStartSprite))
+ mux.HandleFunc("POST /api/sprite/stop", s.requireAuth(s.handleStopSprite))
+ mux.HandleFunc("DELETE /api/sprite", s.requireAuth(s.handleDestroySprite))
+ mux.HandleFunc("GET /api/sprite/ssh-config", s.requireAuth(s.handleGetSSHConfig))
+ mux.HandleFunc("GET /api/export", s.requireAuth(s.handleExport))
+
+ // Proxied routes to sprite
+ mux.HandleFunc("GET /api/tasks", s.requireAuth(s.handleProxyTasks))
+ mux.HandleFunc("POST /api/tasks", s.requireAuth(s.handleProxyTasks))
+ mux.HandleFunc("GET /api/tasks/{id}", s.requireAuth(s.handleProxyTasks))
+ mux.HandleFunc("PUT /api/tasks/{id}", s.requireAuth(s.handleProxyTasks))
+ mux.HandleFunc("DELETE /api/tasks/{id}", s.requireAuth(s.handleProxyTasks))
+ mux.HandleFunc("POST /api/tasks/{id}/queue", s.requireAuth(s.handleProxyTasks))
+ mux.HandleFunc("POST /api/tasks/{id}/retry", s.requireAuth(s.handleProxyTasks))
+ mux.HandleFunc("POST /api/tasks/{id}/close", s.requireAuth(s.handleProxyTasks))
+
+ mux.HandleFunc("GET /api/projects", s.requireAuth(s.handleProxyTasks))
+ mux.HandleFunc("POST /api/projects", s.requireAuth(s.handleProxyTasks))
+ mux.HandleFunc("GET /api/projects/{id}", s.requireAuth(s.handleProxyTasks))
+ mux.HandleFunc("PUT /api/projects/{id}", s.requireAuth(s.handleProxyTasks))
+ mux.HandleFunc("DELETE /api/projects/{id}", s.requireAuth(s.handleProxyTasks))
+
+ mux.HandleFunc("GET /api/settings", s.requireAuth(s.handleProxyTasks))
+ mux.HandleFunc("PUT /api/settings", s.requireAuth(s.handleProxyTasks))
+
+ // WebSocket endpoint
+ mux.HandleFunc("GET /api/ws", s.requireAuth(s.handleWebSocket))
+
+ // Health check
+ mux.HandleFunc("GET /health", s.handleHealth)
+
+ // Static files (React app)
+ if s.staticFS != nil {
+ mux.Handle("/", s.staticFileHandler())
+ }
+
+ // Apply middleware
+ handler := s.corsMiddleware(s.loggingMiddleware(mux))
+
+ server := &http.Server{
+ Addr: s.addr,
+ Handler: handler,
+ ReadTimeout: 15 * time.Second,
+ WriteTimeout: 15 * time.Second,
+ IdleTimeout: 60 * time.Second,
+ }
+
+ s.logger.Info("starting server", "addr", s.addr)
+
+ // Start server in goroutine
+ errCh := make(chan error, 1)
+ go func() {
+ if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ errCh <- err
+ }
+ }()
+
+ // Wait for context cancellation or error
+ select {
+ case <-ctx.Done():
+ shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+ return server.Shutdown(shutdownCtx)
+ case err := <-errCh:
+ return err
+ }
+}
+
+// staticFileHandler returns a handler for serving static files with SPA support.
+func (s *Server) staticFileHandler() http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Clean the path
+ p := path.Clean(r.URL.Path)
+ if p == "/" {
+ p = "/index.html"
+ }
+
+ // Try to open the file
+ filePath := strings.TrimPrefix(p, "/")
+ f, err := s.staticFS.Open(filePath)
+ if err != nil {
+ // File not found, serve index.html for SPA routing
+ f, err = s.staticFS.Open("index.html")
+ if err != nil {
+ http.NotFound(w, r)
+ return
+ }
+ filePath = "index.html"
+ }
+ defer f.Close()
+
+ // Get file info
+ stat, err := f.Stat()
+ if err != nil {
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ // If it's a directory, serve index.html
+ if stat.IsDir() {
+ f.Close()
+ indexPath := path.Join(filePath, "index.html")
+ f, err = s.staticFS.Open(indexPath)
+ if err != nil {
+ // Fallback to root index.html for SPA
+ f, err = s.staticFS.Open("index.html")
+ if err != nil {
+ http.NotFound(w, r)
+ return
+ }
+ }
+ }
+
+ // Set content type based on extension
+ contentType := getContentType(p)
+ if contentType != "" {
+ w.Header().Set("Content-Type", contentType)
+ }
+
+ // Read file content and serve
+ content, err := io.ReadAll(f)
+ if err != nil {
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ w.Write(content)
+ })
+}
+
+// getContentType returns the content type for a file extension.
+func getContentType(path string) string {
+ switch {
+ case strings.HasSuffix(path, ".html"):
+ return "text/html; charset=utf-8"
+ case strings.HasSuffix(path, ".css"):
+ return "text/css; charset=utf-8"
+ case strings.HasSuffix(path, ".js"):
+ return "application/javascript"
+ case strings.HasSuffix(path, ".json"):
+ return "application/json"
+ case strings.HasSuffix(path, ".svg"):
+ return "image/svg+xml"
+ case strings.HasSuffix(path, ".png"):
+ return "image/png"
+ case strings.HasSuffix(path, ".ico"):
+ return "image/x-icon"
+ default:
+ return ""
+ }
+}
+
+// handleHealth handles health check requests.
+func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("ok"))
+}
diff --git a/web/eslint.config.js b/web/eslint.config.js
new file mode 100644
index 00000000..5e6e247f
--- /dev/null
+++ b/web/eslint.config.js
@@ -0,0 +1,29 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+
+export default tseslint.config(
+ { ignores: ['dist'] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ['**/*.{ts,tsx}'],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ plugins: {
+ 'react-hooks': reactHooks,
+ 'react-refresh': reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
+ },
+ },
+)
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 00000000..a3ddbacd
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ taskyou
+
+
+
+
+
+
diff --git a/web/package-lock.json b/web/package-lock.json
new file mode 100644
index 00000000..56b14965
--- /dev/null
+++ b/web/package-lock.json
@@ -0,0 +1,5693 @@
+{
+ "name": "taskyou-web",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "taskyou-web",
+ "version": "0.1.0",
+ "dependencies": {
+ "@radix-ui/react-avatar": "^1.1.2",
+ "@radix-ui/react-dialog": "^1.1.4",
+ "@radix-ui/react-dropdown-menu": "^2.1.4",
+ "@radix-ui/react-label": "^2.1.1",
+ "@radix-ui/react-scroll-area": "^1.2.2",
+ "@radix-ui/react-select": "^2.1.4",
+ "@radix-ui/react-separator": "^1.1.1",
+ "@radix-ui/react-slot": "^1.1.1",
+ "@radix-ui/react-tabs": "^1.1.2",
+ "@radix-ui/react-toast": "^1.2.4",
+ "@radix-ui/react-tooltip": "^1.1.6",
+ "@xterm/addon-attach": "^0.12.0",
+ "@xterm/addon-fit": "^0.11.0",
+ "@xterm/xterm": "^6.0.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "framer-motion": "^11.18.2",
+ "lucide-react": "^0.469.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-router-dom": "^7.1.1",
+ "tailwind-merge": "^2.6.0",
+ "tailwindcss-animate": "^1.0.7"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.17.0",
+ "@types/react": "^19.0.2",
+ "@types/react-dom": "^19.0.2",
+ "@vitejs/plugin-react": "^4.3.4",
+ "autoprefixer": "^10.4.20",
+ "eslint": "^9.17.0",
+ "eslint-plugin-react-hooks": "^5.1.0",
+ "eslint-plugin-react-refresh": "^0.4.16",
+ "globals": "^15.14.0",
+ "postcss": "^8.4.49",
+ "tailwindcss": "^3.4.17",
+ "typescript": "~5.6.2",
+ "typescript-eslint": "^8.18.2",
+ "vite": "^6.0.5"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
+ "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz",
+ "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
+ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/generator": "^7.28.6",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz",
+ "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+ "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
+ "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.28.6"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz",
+ "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/generator": "^7.28.6",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.6",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
+ "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
+ "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
+ "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
+ "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+ "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
+ "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.4"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
+ "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz",
+ "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.4",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
+ "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "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-collection/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz",
+ "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
+ "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "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-dropdown-menu/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz",
+ "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
+ "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "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-menu/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
+ "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "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-popper/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
+ "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz",
+ "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "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-scroll-area/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
+ "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "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-select/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz",
+ "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
+ "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+ "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toast": {
+ "version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz",
+ "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "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-toast/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
+ "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "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-tooltip/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-is-hydrated": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
+ "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
+ "dependencies": {
+ "use-sync-external-store": "^1.5.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
+ "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
+ "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-rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
+ "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
+ "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
+ "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
+ "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
+ "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
+ "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
+ "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
+ "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
+ "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
+ "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
+ "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
+ "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
+ "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
+ "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
+ "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
+ "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
+ "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
+ "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
+ "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
+ "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
+ "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
+ "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
+ "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
+ "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
+ "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz",
+ "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==",
+ "devOptional": true,
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "devOptional": true,
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz",
+ "integrity": "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.53.0",
+ "@typescript-eslint/type-utils": "8.53.0",
+ "@typescript-eslint/utils": "8.53.0",
+ "@typescript-eslint/visitor-keys": "8.53.0",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.53.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.0.tgz",
+ "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.53.0",
+ "@typescript-eslint/types": "8.53.0",
+ "@typescript-eslint/typescript-estree": "8.53.0",
+ "@typescript-eslint/visitor-keys": "8.53.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.0.tgz",
+ "integrity": "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.53.0",
+ "@typescript-eslint/types": "^8.53.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz",
+ "integrity": "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.53.0",
+ "@typescript-eslint/visitor-keys": "8.53.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz",
+ "integrity": "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.0.tgz",
+ "integrity": "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.53.0",
+ "@typescript-eslint/typescript-estree": "8.53.0",
+ "@typescript-eslint/utils": "8.53.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.0.tgz",
+ "integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz",
+ "integrity": "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.53.0",
+ "@typescript-eslint/tsconfig-utils": "8.53.0",
+ "@typescript-eslint/types": "8.53.0",
+ "@typescript-eslint/visitor-keys": "8.53.0",
+ "debug": "^4.4.3",
+ "minimatch": "^9.0.5",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.0.tgz",
+ "integrity": "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.53.0",
+ "@typescript-eslint/types": "8.53.0",
+ "@typescript-eslint/typescript-estree": "8.53.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz",
+ "integrity": "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.53.0",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/@xterm/addon-attach": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@xterm/addon-attach/-/addon-attach-0.12.0.tgz",
+ "integrity": "sha512-1lxvXM4JYSm60lbFmE8WMOy2oF2ip3Ye8jWorSAmwy7x8FiC53netEJ5RguL8+FSRj79MUQsNCb2hprY2QA2ig==",
+ "license": "MIT"
+ },
+ "node_modules/@xterm/addon-fit": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz",
+ "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
+ "license": "MIT"
+ },
+ "node_modules/@xterm/xterm": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
+ "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==",
+ "license": "MIT",
+ "workspaces": [
+ "addons/*"
+ ]
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.23",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
+ "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "browserslist": "^4.28.1",
+ "caniuse-lite": "^1.0.30001760",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.14",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz",
+ "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==",
+ "dev": true,
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001764",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz",
+ "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ]
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "devOptional": true
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.267",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
+ "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
+ "dev": true
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
+ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.1",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.39.2",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
+ "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.26",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz",
+ "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==",
+ "dev": true,
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/framer-motion": {
+ "version": "11.18.2",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz",
+ "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==",
+ "dependencies": {
+ "motion-dom": "^11.18.1",
+ "motion-utils": "^11.18.1",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "15.15.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
+ "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.469.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.469.0.tgz",
+ "integrity": "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/motion-dom": {
+ "version": "11.18.1",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz",
+ "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==",
+ "dependencies": {
+ "motion-utils": "^11.18.1"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "11.18.1",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz",
+ "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA=="
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/react": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
+ "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.3"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.12.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
+ "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.12.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz",
+ "integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==",
+ "dependencies": {
+ "react-router": "7.12.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
+ "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.55.1",
+ "@rollup/rollup-android-arm64": "4.55.1",
+ "@rollup/rollup-darwin-arm64": "4.55.1",
+ "@rollup/rollup-darwin-x64": "4.55.1",
+ "@rollup/rollup-freebsd-arm64": "4.55.1",
+ "@rollup/rollup-freebsd-x64": "4.55.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.55.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.55.1",
+ "@rollup/rollup-linux-arm64-musl": "4.55.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.55.1",
+ "@rollup/rollup-linux-loong64-musl": "4.55.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.55.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.55.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.55.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.55.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.55.1",
+ "@rollup/rollup-linux-x64-gnu": "4.55.1",
+ "@rollup/rollup-linux-x64-musl": "4.55.1",
+ "@rollup/rollup-openbsd-x64": "4.55.1",
+ "@rollup/rollup-openharmony-arm64": "4.55.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.55.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.55.1",
+ "@rollup/rollup-win32-x64-gnu": "4.55.1",
+ "@rollup/rollup-win32-x64-msvc": "4.55.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwind-merge": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
+ "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
+ "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
+ "dev": true,
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.6.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.0.tgz",
+ "integrity": "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.53.0",
+ "@typescript-eslint/parser": "8.53.0",
+ "@typescript-eslint/typescript-estree": "8.53.0",
+ "@typescript-eslint/utils": "8.53.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+ },
+ "node_modules/vite": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 00000000..f5f2e522
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "taskyou-web",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@radix-ui/react-avatar": "^1.1.2",
+ "@radix-ui/react-dialog": "^1.1.4",
+ "@radix-ui/react-dropdown-menu": "^2.1.4",
+ "@radix-ui/react-label": "^2.1.1",
+ "@radix-ui/react-scroll-area": "^1.2.2",
+ "@radix-ui/react-select": "^2.1.4",
+ "@radix-ui/react-separator": "^1.1.1",
+ "@radix-ui/react-slot": "^1.1.1",
+ "@radix-ui/react-tabs": "^1.1.2",
+ "@radix-ui/react-toast": "^1.2.4",
+ "@radix-ui/react-tooltip": "^1.1.6",
+ "@xterm/addon-attach": "^0.12.0",
+ "@xterm/addon-fit": "^0.11.0",
+ "@xterm/xterm": "^6.0.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "framer-motion": "^11.18.2",
+ "lucide-react": "^0.469.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-router-dom": "^7.1.1",
+ "tailwind-merge": "^2.6.0",
+ "tailwindcss-animate": "^1.0.7"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.17.0",
+ "@types/react": "^19.0.2",
+ "@types/react-dom": "^19.0.2",
+ "@vitejs/plugin-react": "^4.3.4",
+ "autoprefixer": "^10.4.20",
+ "eslint": "^9.17.0",
+ "eslint-plugin-react-hooks": "^5.1.0",
+ "eslint-plugin-react-refresh": "^0.4.16",
+ "globals": "^15.14.0",
+ "postcss": "^8.4.49",
+ "tailwindcss": "^3.4.17",
+ "typescript": "~5.6.2",
+ "typescript-eslint": "^8.18.2",
+ "vite": "^6.0.5"
+ }
+}
diff --git a/web/postcss.config.js b/web/postcss.config.js
new file mode 100644
index 00000000..2e7af2b7
--- /dev/null
+++ b/web/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/web/src/App.tsx b/web/src/App.tsx
new file mode 100644
index 00000000..f8ee62cd
--- /dev/null
+++ b/web/src/App.tsx
@@ -0,0 +1,41 @@
+import { useState } from 'react';
+import { useAuth } from './hooks/useAuth';
+import { LoginPage } from './pages/LoginPage';
+import { DashboardPage } from './pages/DashboardPage';
+import { SettingsPage } from './pages/SettingsPage';
+
+type View = 'dashboard' | 'settings';
+
+function App() {
+ const { user, loading, logout } = useAuth();
+ const [view, setView] = useState('dashboard');
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!user) {
+ return ;
+ }
+
+ if (view === 'settings') {
+ return setView('dashboard')} />;
+ }
+
+ return (
+ setView('settings')}
+ />
+ );
+}
+
+export default App;
diff --git a/web/src/api/client.ts b/web/src/api/client.ts
new file mode 100644
index 00000000..d5886a87
--- /dev/null
+++ b/web/src/api/client.ts
@@ -0,0 +1,161 @@
+import type {
+ User,
+ Sprite,
+ Task,
+ CreateTaskRequest,
+ UpdateTaskRequest,
+ Project,
+ CreateProjectRequest,
+ UpdateProjectRequest,
+ TaskLog,
+} from './types';
+
+// In development, use the local API server. In production, use relative /api path.
+const API_BASE = import.meta.env.DEV ? 'http://localhost:8081' : '/api';
+
+async function fetchJSON(path: string, options?: RequestInit): Promise {
+ const response = await fetch(`${API_BASE}${path}`, {
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options?.headers,
+ },
+ credentials: 'include',
+ });
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({ error: response.statusText }));
+ throw new Error(error.error || 'Request failed');
+ }
+
+ // Handle 204 No Content
+ if (response.status === 204) {
+ return undefined as T;
+ }
+
+ return response.json();
+}
+
+// Auth API
+export const auth = {
+ getMe: () => fetchJSON('/auth/me'),
+ logout: () => fetchJSON<{ success: boolean }>('/auth/logout', { method: 'POST' }),
+};
+
+// Sprite API
+export const sprite = {
+ get: () => fetchJSON('/sprite'),
+ create: () => fetchJSON('/sprite', { method: 'POST' }),
+ start: () => fetchJSON<{ success: boolean }>('/sprite/start', { method: 'POST' }),
+ stop: () => fetchJSON<{ success: boolean }>('/sprite/stop', { method: 'POST' }),
+ destroy: () => fetchJSON<{ success: boolean }>('/sprite', { method: 'DELETE' }),
+ getSSHConfig: async () => {
+ const response = await fetch(`${API_BASE}/sprite/ssh-config`, {
+ credentials: 'include',
+ });
+ if (!response.ok) {
+ throw new Error('Failed to get SSH config');
+ }
+ return response.text();
+ },
+};
+
+// Tasks API
+export const tasks = {
+ list: (options?: { status?: string; project?: string; type?: string; all?: boolean }) => {
+ const params = new URLSearchParams();
+ if (options?.status) params.set('status', options.status);
+ if (options?.project) params.set('project', options.project);
+ if (options?.type) params.set('type', options.type);
+ if (options?.all) params.set('all', 'true');
+ const query = params.toString();
+ return fetchJSON(`/tasks${query ? `?${query}` : ''}`);
+ },
+ get: (id: number) => fetchJSON(`/tasks/${id}`),
+ create: (data: CreateTaskRequest) =>
+ fetchJSON('/tasks', {
+ method: 'POST',
+ body: JSON.stringify(data),
+ }),
+ update: (id: number, data: UpdateTaskRequest) =>
+ fetchJSON(`/tasks/${id}`, {
+ method: 'PUT',
+ body: JSON.stringify(data),
+ }),
+ delete: (id: number) =>
+ fetchJSON(`/tasks/${id}`, { method: 'DELETE' }),
+ queue: (id: number) =>
+ fetchJSON(`/tasks/${id}/queue`, { method: 'POST' }),
+ retry: (id: number, feedback?: string) =>
+ fetchJSON(`/tasks/${id}/retry`, {
+ method: 'POST',
+ body: JSON.stringify({ feedback }),
+ }),
+ close: (id: number) =>
+ fetchJSON(`/tasks/${id}/close`, { method: 'POST' }),
+ getLogs: (id: number, limit?: number) => {
+ const params = limit ? `?limit=${limit}` : '';
+ return fetchJSON(`/tasks/${id}/logs${params}`);
+ },
+ getTerminal: (id: number) =>
+ fetchJSON(`/tasks/${id}/terminal`),
+ stopTerminal: (id: number) =>
+ fetchJSON(`/tasks/${id}/terminal`, { method: 'DELETE' }),
+};
+
+// Terminal info returned by the API
+export interface TerminalInfo {
+ task_id: number;
+ port: number;
+ tmux_target: string;
+ websocket_url: string;
+}
+
+// Projects API
+export const projects = {
+ list: () => fetchJSON('/projects'),
+ get: (id: number) => fetchJSON(`/projects/${id}`),
+ create: (data: CreateProjectRequest) =>
+ fetchJSON('/projects', {
+ method: 'POST',
+ body: JSON.stringify(data),
+ }),
+ update: (id: number, data: UpdateProjectRequest) =>
+ fetchJSON(`/projects/${id}`, {
+ method: 'PUT',
+ body: JSON.stringify(data),
+ }),
+ delete: (id: number) =>
+ fetchJSON(`/projects/${id}`, { method: 'DELETE' }),
+};
+
+// Settings API
+export const settings = {
+ get: () => fetchJSON>('/settings'),
+ update: (data: Record) =>
+ fetchJSON<{ success: boolean }>('/settings', {
+ method: 'PUT',
+ body: JSON.stringify(data),
+ }),
+};
+
+// Export API
+export const exportData = {
+ download: async () => {
+ const response = await fetch(`${API_BASE}/export`, {
+ credentials: 'include',
+ });
+ if (!response.ok) {
+ throw new Error('Failed to export data');
+ }
+ const blob = await response.blob();
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'tasks-export.db';
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ },
+};
diff --git a/web/src/api/types.ts b/web/src/api/types.ts
new file mode 100644
index 00000000..6b2a0ce7
--- /dev/null
+++ b/web/src/api/types.ts
@@ -0,0 +1,102 @@
+// User and authentication types
+export interface User {
+ id: string;
+ email: string;
+ name: string;
+ avatar_url: string;
+ sprite?: Sprite | null;
+}
+
+export interface Sprite {
+ id: string;
+ name: string;
+ status: 'pending' | 'creating' | 'running' | 'stopped' | 'error';
+ region: string;
+ created_at: string;
+}
+
+// Task types
+export type TaskStatus = 'backlog' | 'queued' | 'processing' | 'blocked' | 'done';
+
+export interface Task {
+ id: number;
+ title: string;
+ body: string;
+ status: TaskStatus;
+ type: string;
+ project: string;
+ worktree_path?: string;
+ branch_name?: string;
+ port?: number;
+ pr_url?: string;
+ pr_number?: number;
+ dangerous_mode: boolean;
+ scheduled_at?: string;
+ recurrence?: string;
+ last_run_at?: string;
+ created_at: string;
+ updated_at: string;
+ started_at?: string;
+ completed_at?: string;
+}
+
+export interface CreateTaskRequest {
+ title: string;
+ body?: string;
+ type?: string;
+ project?: string;
+ scheduled_at?: string;
+ recurrence?: string;
+}
+
+export interface UpdateTaskRequest {
+ title?: string;
+ body?: string;
+ status?: TaskStatus;
+ type?: string;
+ project?: string;
+ scheduled_at?: string;
+ recurrence?: string;
+}
+
+// Project types
+export interface Project {
+ id: number;
+ name: string;
+ path: string;
+ aliases: string;
+ instructions: string;
+ color: string;
+ created_at: string;
+}
+
+export interface CreateProjectRequest {
+ name: string;
+ path: string;
+ aliases?: string;
+ instructions?: string;
+ color?: string;
+}
+
+export interface UpdateProjectRequest {
+ name?: string;
+ path?: string;
+ aliases?: string;
+ instructions?: string;
+ color?: string;
+}
+
+// Task log types
+export interface TaskLog {
+ id: number;
+ task_id: number;
+ line_type: 'system' | 'text' | 'tool' | 'error' | 'output';
+ content: string;
+ created_at: string;
+}
+
+// WebSocket message types
+export type WebSocketMessage =
+ | { type: 'task_update'; data: Task }
+ | { type: 'task_deleted'; data: { id: number } }
+ | { type: 'task_log'; data: { task_id: number; log: TaskLog } };
diff --git a/web/src/components/CommandPalette.tsx b/web/src/components/CommandPalette.tsx
new file mode 100644
index 00000000..b520fe8f
--- /dev/null
+++ b/web/src/components/CommandPalette.tsx
@@ -0,0 +1,314 @@
+import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import {
+ Search,
+ Clock,
+ Zap,
+ AlertCircle,
+ CheckCircle,
+ Plus,
+ Settings,
+ ArrowRight,
+ Command,
+} from 'lucide-react';
+import type { Task } from '@/api/types';
+import { cn } from '@/lib/utils';
+
+interface CommandPaletteProps {
+ isOpen: boolean;
+ onClose: () => void;
+ tasks: Task[];
+ onSelectTask: (task: Task) => void;
+ onNewTask: () => void;
+ onSettings: () => void;
+}
+
+interface CommandItem {
+ id: string;
+ type: 'task' | 'action';
+ task?: Task;
+ label: string;
+ description?: string;
+ icon: React.ElementType;
+ iconColor?: string;
+ action?: () => void;
+}
+
+const statusIcons: Record = {
+ backlog: { icon: Clock, color: 'text-[hsl(var(--status-backlog))]' },
+ queued: { icon: Clock, color: 'text-[hsl(var(--status-queued))]' },
+ processing: { icon: Zap, color: 'text-[hsl(var(--status-processing))]' },
+ blocked: { icon: AlertCircle, color: 'text-[hsl(var(--status-blocked))]' },
+ done: { icon: CheckCircle, color: 'text-[hsl(var(--status-done))]' },
+};
+
+const defaultStatusIcon = { icon: Clock, color: 'text-muted-foreground' };
+
+export function CommandPalette({
+ isOpen,
+ onClose,
+ tasks,
+ onSelectTask,
+ onNewTask,
+ onSettings,
+}: CommandPaletteProps) {
+ const [query, setQuery] = useState('');
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ const inputRef = useRef(null);
+ const listRef = useRef(null);
+
+ // Reset state when opened
+ useEffect(() => {
+ if (isOpen) {
+ setQuery('');
+ setSelectedIndex(0);
+ setTimeout(() => inputRef.current?.focus(), 50);
+ }
+ }, [isOpen]);
+
+ // Build filtered items
+ const items = useMemo(() => {
+ const actions: CommandItem[] = [
+ {
+ id: 'new-task',
+ type: 'action',
+ label: 'New Task',
+ description: 'Create a new task',
+ icon: Plus,
+ iconColor: 'text-primary',
+ action: () => {
+ onClose();
+ onNewTask();
+ },
+ },
+ {
+ id: 'settings',
+ type: 'action',
+ label: 'Settings',
+ description: 'Open settings',
+ icon: Settings,
+ iconColor: 'text-muted-foreground',
+ action: () => {
+ onClose();
+ onSettings();
+ },
+ },
+ ];
+
+ const taskItems: CommandItem[] = tasks.map((task) => {
+ const statusInfo = statusIcons[task.status] || defaultStatusIcon;
+ return {
+ id: `task-${task.id}`,
+ type: 'task',
+ task,
+ label: task.title,
+ description: task.project !== 'personal' ? task.project : undefined,
+ icon: statusInfo.icon,
+ iconColor: statusInfo.color,
+ action: () => {
+ onClose();
+ onSelectTask(task);
+ },
+ };
+ });
+
+ if (!query.trim()) {
+ // Show recent/active tasks first, then actions
+ const activeTasks = taskItems.filter(
+ (t) => t.task && ['processing', 'queued', 'blocked'].includes(t.task.status)
+ );
+ const recentTasks = taskItems
+ .filter((t) => t.task && !['processing', 'queued', 'blocked'].includes(t.task.status))
+ .slice(0, 5);
+
+ return [...actions, ...activeTasks, ...recentTasks];
+ }
+
+ const lowerQuery = query.toLowerCase();
+
+ // Filter tasks by query
+ const filteredTasks = taskItems.filter((item) => {
+ if (!item.task) return false;
+ const task = item.task;
+ return (
+ task.title.toLowerCase().includes(lowerQuery) ||
+ task.project.toLowerCase().includes(lowerQuery) ||
+ task.id.toString().includes(lowerQuery) ||
+ (task.pr_number && task.pr_number.toString().includes(lowerQuery)) ||
+ (task.pr_url && task.pr_url.toLowerCase().includes(lowerQuery))
+ );
+ });
+
+ // Filter actions by query
+ const filteredActions = actions.filter(
+ (item) => item.label.toLowerCase().includes(lowerQuery)
+ );
+
+ return [...filteredActions, ...filteredTasks];
+ }, [tasks, query, onClose, onSelectTask, onNewTask, onSettings]);
+
+ // Clamp selected index
+ useEffect(() => {
+ if (selectedIndex >= items.length) {
+ setSelectedIndex(Math.max(0, items.length - 1));
+ }
+ }, [items.length, selectedIndex]);
+
+ // Scroll selected item into view
+ useEffect(() => {
+ const selectedEl = listRef.current?.children[selectedIndex] as HTMLElement;
+ selectedEl?.scrollIntoView({ block: 'nearest' });
+ }, [selectedIndex]);
+
+ const handleKeyDown = useCallback(
+ (e: React.KeyboardEvent) => {
+ switch (e.key) {
+ case 'ArrowDown':
+ e.preventDefault();
+ setSelectedIndex((i) => Math.min(i + 1, items.length - 1));
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ setSelectedIndex((i) => Math.max(i - 1, 0));
+ break;
+ case 'Enter':
+ e.preventDefault();
+ items[selectedIndex]?.action?.();
+ break;
+ case 'Escape':
+ e.preventDefault();
+ onClose();
+ break;
+ }
+ },
+ [items, selectedIndex, onClose]
+ );
+
+ // Global keyboard shortcut
+ useEffect(() => {
+ const handleGlobalKeyDown = (e: KeyboardEvent) => {
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
+ e.preventDefault();
+ if (isOpen) {
+ onClose();
+ }
+ }
+ };
+
+ window.addEventListener('keydown', handleGlobalKeyDown);
+ return () => window.removeEventListener('keydown', handleGlobalKeyDown);
+ }, [isOpen, onClose]);
+
+ return (
+
+ {isOpen && (
+
+ {/* Backdrop */}
+
+
+ {/* Dialog */}
+
+ {/* Search input */}
+
+
+ {
+ setQuery(e.target.value);
+ setSelectedIndex(0);
+ }}
+ onKeyDown={handleKeyDown}
+ placeholder="Search tasks, run actions..."
+ className="flex-1 bg-transparent text-base outline-none placeholder:text-muted-foreground"
+ />
+
+ K
+
+
+
+ {/* Results */}
+
+ {items.length === 0 ? (
+
+ No results found for "{query}"
+
+ ) : (
+ items.map((item, index) => {
+ const Icon = item.icon;
+ const isSelected = index === selectedIndex;
+
+ return (
+
+ );
+ })
+ )}
+
+
+ {/* Footer */}
+
+
+
+ ↑↓
+ navigate
+
+
+ ↵
+ select
+
+
+ esc
+ close
+
+
+
{items.length} results
+
+
+
+ )}
+
+ );
+}
diff --git a/web/src/components/Terminal.tsx b/web/src/components/Terminal.tsx
new file mode 100644
index 00000000..00b4c29b
--- /dev/null
+++ b/web/src/components/Terminal.tsx
@@ -0,0 +1,246 @@
+import { useEffect, useRef, useState, useCallback } from 'react';
+import { Terminal as XTerm } from '@xterm/xterm';
+import { FitAddon } from '@xterm/addon-fit';
+import { Loader2, AlertCircle, Terminal as TerminalIcon, RefreshCw, Download } from 'lucide-react';
+import { tasks } from '@/api/client';
+import { cn } from '@/lib/utils';
+import '@xterm/xterm/css/xterm.css';
+
+interface TerminalProps {
+ taskId: number;
+ className?: string;
+}
+
+type TerminalState = 'connecting' | 'connected' | 'disconnected' | 'error' | 'no-session' | 'no-ttyd';
+
+export function TaskTerminal({ taskId, className }: TerminalProps) {
+ const containerRef = useRef(null);
+ const terminalRef = useRef(null);
+ const fitAddonRef = useRef(null);
+ const wsRef = useRef(null);
+ const [state, setState] = useState('connecting');
+ const [error, setError] = useState(null);
+
+ const connect = useCallback(async () => {
+ if (!containerRef.current) return;
+
+ setState('connecting');
+ setError(null);
+
+ try {
+ // Get terminal connection info from the API
+ const terminalInfo = await tasks.getTerminal(taskId);
+
+ // Clean up any existing terminal
+ if (terminalRef.current) {
+ terminalRef.current.dispose();
+ terminalRef.current = null;
+ }
+ if (wsRef.current) {
+ wsRef.current.close();
+ wsRef.current = null;
+ }
+
+ // Create terminal instance
+ const terminal = new XTerm({
+ cursorBlink: true,
+ fontSize: 14,
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
+ theme: {
+ background: '#0f172a',
+ foreground: '#e2e8f0',
+ cursor: '#e2e8f0',
+ cursorAccent: '#0f172a',
+ selectionBackground: '#334155',
+ black: '#1e293b',
+ red: '#ef4444',
+ green: '#22c55e',
+ yellow: '#eab308',
+ blue: '#3b82f6',
+ magenta: '#a855f7',
+ cyan: '#06b6d4',
+ white: '#f8fafc',
+ brightBlack: '#475569',
+ brightRed: '#f87171',
+ brightGreen: '#4ade80',
+ brightYellow: '#facc15',
+ brightBlue: '#60a5fa',
+ brightMagenta: '#c084fc',
+ brightCyan: '#22d3ee',
+ brightWhite: '#ffffff',
+ },
+ });
+
+ terminalRef.current = terminal;
+
+ // Add fit addon
+ const fitAddon = new FitAddon();
+ fitAddonRef.current = fitAddon;
+ terminal.loadAddon(fitAddon);
+
+ // Open terminal in container
+ terminal.open(containerRef.current);
+ fitAddon.fit();
+
+ // Connect to ttyd websocket
+ const ws = new WebSocket(terminalInfo.websocket_url);
+ wsRef.current = ws;
+
+ ws.onopen = () => {
+ setState('connected');
+ terminal.focus();
+
+ // Send terminal size to ttyd
+ // ttyd protocol: byte 1 + JSON for resize (using actual byte values, not ASCII)
+ const sendSize = () => {
+ const msg = JSON.stringify({ columns: terminal.cols, rows: terminal.rows });
+ ws.send(String.fromCharCode(1) + msg);
+ };
+ sendSize();
+ terminal.onResize(sendSize);
+ };
+
+ // Handle incoming data from ttyd
+ ws.onmessage = (event) => {
+ const data = event.data;
+ if (typeof data === 'string' && data.length > 0) {
+ const cmd = data.charCodeAt(0);
+ const payload = data.substring(1);
+ if (cmd === 0) {
+ // Output message - write to terminal
+ terminal.write(payload);
+ }
+ // cmd 1 = window title, 2 = preferences (ignored)
+ }
+ };
+
+ // Send input to ttyd
+ // ttyd protocol: byte 0 + input data (using actual byte values, not ASCII)
+ terminal.onData((data) => {
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.send(String.fromCharCode(0) + data);
+ }
+ });
+
+ ws.onclose = () => {
+ if (state !== 'error') {
+ setState('disconnected');
+ }
+ };
+
+ ws.onerror = () => {
+ setState('error');
+ setError('WebSocket connection failed');
+ };
+ } catch (err) {
+ const message = err instanceof Error ? err.message : 'Failed to connect';
+ if (message.includes('no active terminal') || message.includes('not running')) {
+ setState('no-session');
+ } else if (message.includes('ttyd not installed')) {
+ setState('no-ttyd');
+ } else {
+ setState('error');
+ setError(message);
+ }
+ }
+ }, [taskId, state]);
+
+ // Connect on mount and when taskId changes
+ useEffect(() => {
+ connect();
+
+ return () => {
+ if (terminalRef.current) {
+ terminalRef.current.dispose();
+ terminalRef.current = null;
+ }
+ if (wsRef.current) {
+ wsRef.current.close();
+ wsRef.current = null;
+ }
+ };
+ }, [taskId]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // Handle window resize
+ useEffect(() => {
+ const handleResize = () => {
+ if (fitAddonRef.current && terminalRef.current) {
+ fitAddonRef.current.fit();
+ }
+ };
+
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ // Render overlay for non-connected states
+ const renderOverlay = () => {
+ if (state === 'connected') return null;
+
+ return (
+
+ {state === 'connecting' && (
+ <>
+
+
Connecting to terminal...
+ >
+ )}
+ {state === 'no-session' && (
+ <>
+
+
No active terminal session
+
+ The task must be running to view its terminal
+
+ >
+ )}
+ {state === 'no-ttyd' && (
+ <>
+
+
ttyd not installed
+
+ Install ttyd to enable live terminal streaming
+
+
+ brew install ttyd
+
+ >
+ )}
+ {state === 'disconnected' && (
+ <>
+
+
Terminal disconnected
+
+ >
+ )}
+ {state === 'error' && (
+ <>
+
+
Connection failed
+
{error}
+
+ >
+ )}
+
+ );
+ };
+
+ return (
+
+ );
+}
diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx
new file mode 100644
index 00000000..f8bbb199
--- /dev/null
+++ b/web/src/components/layout/Header.tsx
@@ -0,0 +1,196 @@
+import { useState, useEffect } from 'react';
+import {
+ Search,
+ Settings,
+ LogOut,
+ Moon,
+ Sun,
+ Monitor,
+ Command,
+ Zap,
+ ChevronDown,
+} from 'lucide-react';
+import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
+import { Button } from '@/components/ui/button';
+import type { User } from '@/api/types';
+
+interface HeaderProps {
+ user: User;
+ onLogout: () => void;
+ onSettings: () => void;
+ onCommandPalette: () => void;
+}
+
+type Theme = 'light' | 'dark' | 'system';
+
+export function Header({ user, onLogout, onSettings, onCommandPalette }: HeaderProps) {
+ const [showUserMenu, setShowUserMenu] = useState(false);
+ const [theme, setTheme] = useState(() => {
+ const stored = localStorage.getItem('theme') as Theme;
+ return stored || 'system';
+ });
+
+ // Apply theme on mount and changes
+ useEffect(() => {
+ const root = document.documentElement;
+ if (theme === 'system') {
+ const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+ root.classList.toggle('dark', systemDark);
+ } else {
+ root.classList.toggle('dark', theme === 'dark');
+ }
+ }, [theme]);
+
+ const handleThemeChange = (newTheme: Theme) => {
+ setTheme(newTheme);
+ localStorage.setItem('theme', newTheme);
+ };
+
+ const getInitials = (name: string) => {
+ return name
+ .split(' ')
+ .map((n) => n[0])
+ .join('')
+ .toUpperCase()
+ .slice(0, 2);
+ };
+
+ const themeIcons = {
+ light: Sun,
+ dark: Moon,
+ system: Monitor,
+ };
+ const ThemeIcon = themeIcons[theme];
+
+ return (
+