diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 0000000..b3ffd6b
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,10 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(go get *)",
+ "Bash(go build *)",
+ "Bash(wails generate *)",
+ "Bash(npm run *)"
+ ]
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4e6a6d1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,47 @@
+# --- Build artifacts ---
+build/bin/
+frontend/dist/
+*.exe
+*.dll
+*.so
+*.dylib
+
+# --- Dependencies / generated ---
+node_modules/
+# wailsjs/ is intentionally tracked — generated by `wails generate module`
+# but consumed by the frontend at build time, so committing it spares
+# fresh clones from a mandatory generate step.
+
+# --- Go ---
+*.test
+*.out
+coverage.out
+go.work
+go.work.sum
+
+# --- IDE / editors ---
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# --- OS noise ---
+.DS_Store
+Thumbs.db
+desktop.ini
+
+# --- Local user data (lives in %APPDATA%/HamLog, but safety net) ---
+*.db
+*.db-journal
+*.db-wal
+*.db-shm
+hamlog.db*
+cty.dat
+cat.log
+
+# --- Secrets ---
+.env
+.env.local
+*.pem
+*.key
diff --git a/app.go b/app.go
new file mode 100644
index 0000000..4ca6e8c
--- /dev/null
+++ b/app.go
@@ -0,0 +1,938 @@
+package main
+
+import (
+ "context"
+ "database/sql"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+
+ "hamlog/internal/adif"
+ "hamlog/internal/cat"
+ "hamlog/internal/db"
+ "hamlog/internal/dxcc"
+ "hamlog/internal/lookup"
+ "hamlog/internal/profile"
+ "hamlog/internal/qso"
+ "hamlog/internal/settings"
+
+ wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
+)
+
+// Setting keys.
+const (
+ keyQRZUser = "lookup.qrz.user"
+ keyQRZPassword = "lookup.qrz.password"
+ keyHQUser = "lookup.hamqth.user"
+ keyHQPassword = "lookup.hamqth.password"
+ keyCacheTTL = "lookup.cache.ttl_days"
+ // Provider routing. Each value is a provider name (qrz | hamqth)
+ // or empty to disable that slot. Primary is consulted first;
+ // Failsafe is the fallback when Primary returns not-found or errs.
+ keyLookupPrimary = "lookup.primary"
+ keyLookupFailsafe = "lookup.failsafe"
+
+ keyStationCallsign = "station.callsign"
+ keyStationOperator = "station.operator"
+ keyStationMyGrid = "station.my_grid"
+ keyStationCountry = "station.my_country"
+ keyStationSOTA = "station.my_sota_ref"
+ keyStationPOTA = "station.my_pota_ref"
+
+ keyListsBands = "lists.bands"
+ keyListsModes = "lists.modes"
+
+ keyCATEnabled = "cat.enabled"
+ keyCATBackend = "cat.backend" // "omnirig" (only one for now)
+ keyCATOmniRigNum = "cat.omnirig.rig" // 1 or 2
+ keyCATPollMs = "cat.poll_ms"
+ keyCATDelayMs = "cat.delay_ms" // pause between commands
+ keyCATDigitalDefault = "cat.digital_default" // mode to use when CAT reports DATA
+)
+
+// CATSettings is the user-tweakable rig-control configuration. Stored as
+// individual key/value pairs to keep the settings table flat.
+type CATSettings struct {
+ Enabled bool `json:"enabled"`
+ Backend string `json:"backend"` // currently always "omnirig"
+ OmniRigNum int `json:"omnirig_rig"` // 1 or 2 (OmniRig "Rig1"/"Rig2" slot)
+ PollMs int `json:"poll_ms"` // poll interval in ms (default 250)
+ DelayMs int `json:"delay_ms"` // pause between commands (default 0)
+ DigitalDefault string `json:"digital_default"` // when CAT says DATA, surface this mode (FT8/FT4/RTTY/…)
+}
+
+// ModePreset is a mode entry with default RST values to auto-populate
+// the entry form when the user picks this mode.
+type ModePreset struct {
+ Name string `json:"name"`
+ DefaultRSTSent string `json:"default_rst_sent,omitempty"`
+ DefaultRSTRcvd string `json:"default_rst_rcvd,omitempty"`
+}
+
+// ListsSettings holds the user-customisable dropdown lists used by the
+// entry form. Default values match common HF/VHF practice.
+type ListsSettings struct {
+ Bands []string `json:"bands"`
+ Modes []ModePreset `json:"modes"`
+}
+
+var defaultBands = []string{
+ "160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m",
+ "12m", "10m", "6m", "2m", "70cm", "23cm",
+}
+var defaultModes = []ModePreset{
+ {Name: "SSB", DefaultRSTSent: "59", DefaultRSTRcvd: "59"},
+ {Name: "CW", DefaultRSTSent: "599", DefaultRSTRcvd: "599"},
+ {Name: "FT8", DefaultRSTSent: "+00", DefaultRSTRcvd: "+00"},
+ {Name: "FT4", DefaultRSTSent: "+00", DefaultRSTRcvd: "+00"},
+ {Name: "RTTY", DefaultRSTSent: "599", DefaultRSTRcvd: "599"},
+ {Name: "PSK31", DefaultRSTSent: "599", DefaultRSTRcvd: "599"},
+ {Name: "AM", DefaultRSTSent: "59", DefaultRSTRcvd: "59"},
+ {Name: "FM", DefaultRSTSent: "59", DefaultRSTRcvd: "59"},
+ {Name: "DIGITALVOICE", DefaultRSTSent: "59", DefaultRSTRcvd: "59"},
+}
+
+// StationSettings holds the active operator profile. Used to stamp every
+// new QSO so we don't ask the user to retype it for each contact.
+// Multi-profile support (portable / SOTA …) will layer on top of this.
+type StationSettings struct {
+ Callsign string `json:"callsign"`
+ Operator string `json:"operator"`
+ MyGrid string `json:"my_grid"`
+ MyCountry string `json:"my_country"`
+ MySOTARef string `json:"my_sota_ref"`
+ MyPOTARef string `json:"my_pota_ref"`
+}
+
+// LookupSettings is the JSON shape exchanged with the frontend.
+// Primary / Failsafe hold a provider name ("qrz" | "hamqth" | "") to
+// route lookups: primary first, failsafe on not-found / error.
+type LookupSettings struct {
+ QRZUser string `json:"qrz_user"`
+ QRZPassword string `json:"qrz_password"`
+ HamQTHUser string `json:"hamqth_user"`
+ HamQTHPassword string `json:"hamqth_password"`
+ Primary string `json:"primary"`
+ Failsafe string `json:"failsafe"`
+ CacheTTLDays int `json:"cache_ttl_days"`
+}
+
+// App is the application context bound to the Wails runtime.
+type App struct {
+ ctx context.Context
+ db *sql.DB
+ qso *qso.Repo
+ settings *settings.Store
+ profiles *profile.Repo
+ lookup *lookup.Manager
+ cache *lookup.Cache
+ cat *cat.Manager
+ dxcc *dxcc.Manager
+ startupErr string // captured for surfacing to the frontend
+ dbPath string
+}
+
+// dxccAdapter bridges *dxcc.Manager to the lookup.DXCCResolver interface
+// without making the lookup package import dxcc.
+type dxccAdapter struct{ m *dxcc.Manager }
+
+func (a dxccAdapter) Resolve(call string) (country, continent string, cqz, ituz int, lat, lon float64, ok bool) {
+ if a.m == nil {
+ return
+ }
+ mm, found := a.m.Lookup(call)
+ if !found || mm.Entity == nil {
+ return
+ }
+ return mm.Entity.Name, mm.Continent, mm.CQZone, mm.ITUZone, mm.Lat, mm.Lon, true
+}
+
+func NewApp() *App { return &App{} }
+
+func (a *App) startup(ctx context.Context) {
+ a.ctx = ctx
+
+ dataDir, err := userDataDir()
+ if err != nil {
+ a.startupErr = "cannot resolve data dir: " + err.Error()
+ fmt.Println("HamLog:", a.startupErr)
+ return
+ }
+ if err := os.MkdirAll(dataDir, 0o755); err != nil {
+ a.startupErr = "cannot create data dir: " + err.Error()
+ fmt.Println("HamLog:", a.startupErr)
+ return
+ }
+ a.dbPath = filepath.Join(dataDir, "hamlog.db")
+ conn, err := db.Open(a.dbPath)
+ if err != nil {
+ a.startupErr = "cannot open db: " + err.Error()
+ fmt.Println("HamLog:", a.startupErr)
+ return
+ }
+ a.db = conn
+ a.qso = qso.NewRepo(conn)
+ a.settings = settings.NewStore(conn)
+ a.profiles = profile.NewRepo(conn)
+ // On first run, copy the legacy single-station settings into a
+ // "Default" profile so the user's existing config carries over without
+ // any manual step. Subsequent runs just confirm an active profile.
+ if _, err := profile.EnsureDefault(a.ctx, conn, a.settings, profile.LegacyStationKeys{
+ Callsign: keyStationCallsign,
+ Operator: keyStationOperator,
+ MyGrid: keyStationMyGrid,
+ Country: keyStationCountry,
+ SOTA: keyStationSOTA,
+ POTA: keyStationPOTA,
+ }); err != nil {
+ fmt.Println("HamLog: EnsureDefault profile:", err)
+ }
+ a.cache = lookup.NewCache(conn, 30*24*time.Hour)
+ a.lookup = lookup.NewManager(a.cache)
+ a.reloadLookupProviders()
+
+ // cty.dat for offline DXCC / country resolution. Cached on disk; first
+ // run downloads it from country-files.com in the background so startup
+ // stays fast even if the network is slow.
+ a.dxcc = dxcc.NewManager(dataDir)
+ a.lookup.SetDXCCResolver(dxccAdapter{m: a.dxcc})
+ go func() {
+ if err := a.dxcc.EnsureLoaded(context.Background()); err != nil {
+ fmt.Println("HamLog: cty.dat unavailable —", err)
+ return
+ }
+ fmt.Println("HamLog: cty.dat loaded —", a.dxcc.Info().Entities, "entities")
+ }()
+ // CAT manager: emit pushes state to the frontend via Wails events.
+ a.cat = cat.NewManager(func(s cat.RigState) {
+ if a.ctx != nil {
+ wruntime.EventsEmit(a.ctx, "cat:state", s)
+ }
+ })
+ a.reloadCAT()
+ fmt.Println("HamLog: db ready at", a.dbPath)
+}
+
+// StartupStatus returns a diagnostic snapshot for the frontend.
+// dbPath is always populated; err is empty when the app is healthy.
+type StartupStatus struct {
+ OK bool `json:"ok"`
+ Err string `json:"err"`
+ DBPath string `json:"db_path"`
+}
+
+// GetStartupStatus exposes whatever happened during startup so the UI
+// can show a useful error instead of just "db not initialized".
+func (a *App) GetStartupStatus() StartupStatus {
+ return StartupStatus{
+ OK: a.startupErr == "",
+ Err: a.startupErr,
+ DBPath: a.dbPath,
+ }
+}
+
+func (a *App) shutdown(ctx context.Context) {
+ if a.db != nil {
+ _ = a.db.Close()
+ }
+}
+
+func userDataDir() (string, error) {
+ base, err := os.UserConfigDir()
+ if err != nil {
+ return "", err
+ }
+ return filepath.Join(base, "HamLog"), nil
+}
+
+// reloadLookupProviders rebuilds the lookup chain from current settings.
+// Called at startup and after the user saves new credentials.
+//
+// Provider order honours the user's primary/failsafe choice. If they
+// haven't picked one yet (fresh install), we default to "primary = first
+// provider with creds" so the app still works out of the box.
+func (a *App) reloadLookupProviders() {
+ if a.lookup == nil {
+ return
+ }
+ m, err := a.settings.GetMany(a.ctx,
+ keyQRZUser, keyQRZPassword, keyHQUser, keyHQPassword,
+ keyCacheTTL, keyLookupPrimary, keyLookupFailsafe)
+ if err != nil {
+ fmt.Println("HamLog: settings load error:", err)
+ return
+ }
+ if days, _ := strconv.Atoi(m[keyCacheTTL]); days > 0 {
+ a.cache.SetTTL(time.Duration(days) * 24 * time.Hour)
+ }
+
+ build := func(name string) lookup.Provider {
+ switch name {
+ case "qrz":
+ if m[keyQRZUser] != "" && m[keyQRZPassword] != "" {
+ return lookup.NewQRZ(m[keyQRZUser], m[keyQRZPassword])
+ }
+ case "hamqth":
+ if m[keyHQUser] != "" && m[keyHQPassword] != "" {
+ return lookup.NewHamQTH(m[keyHQUser], m[keyHQPassword])
+ }
+ }
+ return nil
+ }
+
+ primary, failsafe := m[keyLookupPrimary], m[keyLookupFailsafe]
+ // Fresh install fallback: prefer QRZ over HamQTH when both creds exist.
+ if primary == "" && failsafe == "" {
+ if m[keyQRZUser] != "" && m[keyQRZPassword] != "" {
+ primary = "qrz"
+ if m[keyHQUser] != "" && m[keyHQPassword] != "" {
+ failsafe = "hamqth"
+ }
+ } else if m[keyHQUser] != "" && m[keyHQPassword] != "" {
+ primary = "hamqth"
+ }
+ }
+
+ var providers []lookup.Provider
+ if p := build(primary); p != nil {
+ providers = append(providers, p)
+ }
+ if failsafe != "" && failsafe != primary {
+ if p := build(failsafe); p != nil {
+ providers = append(providers, p)
+ }
+ }
+ a.lookup.SetProviders(providers...)
+}
+
+// --- QSO bindings ---
+
+func (a *App) AddQSO(q qso.QSO) (int64, error) {
+ if a.qso == nil {
+ return 0, fmt.Errorf("db not initialized")
+ }
+ a.applyStationDefaults(&q)
+ return a.qso.Add(a.ctx, q)
+}
+
+// applyStationDefaults fills any empty MY_* / station field on q with the
+// currently-active profile's values. Multi-profile support means a user
+// can be /P with a different callsign + grid + SOTA ref than home — the
+// QSO carries whichever profile was selected at log time.
+func (a *App) applyStationDefaults(q *qso.QSO) {
+ if a.profiles == nil {
+ return
+ }
+ p, err := a.profiles.Active(a.ctx)
+ if err != nil {
+ return
+ }
+ if q.StationCallsign == "" {
+ q.StationCallsign = p.Callsign
+ }
+ if q.Operator == "" {
+ q.Operator = p.Operator
+ }
+ if q.MyGrid == "" {
+ q.MyGrid = p.MyGrid
+ }
+ if q.MyCountry == "" {
+ q.MyCountry = p.MyCountry
+ }
+ if q.MyState == "" {
+ q.MyState = p.MyState
+ }
+ if q.MyCounty == "" {
+ q.MyCounty = p.MyCounty
+ }
+ if q.MyStreet == "" {
+ q.MyStreet = p.MyStreet
+ }
+ if q.MyCity == "" {
+ q.MyCity = p.MyCity
+ }
+ if q.MyPostalCode == "" {
+ q.MyPostalCode = p.MyPostalCode
+ }
+ if q.MySOTARef == "" {
+ q.MySOTARef = p.MySOTARef
+ }
+ if q.MyPOTARef == "" {
+ q.MyPOTARef = p.MyPOTARef
+ }
+ if q.MyRig == "" {
+ q.MyRig = p.MyRig
+ }
+ if q.MyAntenna == "" {
+ q.MyAntenna = p.MyAntenna
+ }
+ if q.TXPower == nil && p.TxPower != nil {
+ v := *p.TxPower
+ q.TXPower = &v
+ }
+}
+
+func (a *App) ListQSO(f qso.ListFilter) ([]qso.QSO, error) {
+ if a.qso == nil {
+ return nil, fmt.Errorf("db not initialized")
+ }
+ return a.qso.List(a.ctx, f)
+}
+
+func (a *App) CountQSO() (int64, error) {
+ if a.qso == nil {
+ return 0, fmt.Errorf("db not initialized")
+ }
+ return a.qso.Count(a.ctx)
+}
+
+func (a *App) GetQSO(id int64) (qso.QSO, error) {
+ if a.qso == nil {
+ return qso.QSO{}, fmt.Errorf("db not initialized")
+ }
+ return a.qso.GetByID(a.ctx, id)
+}
+
+func (a *App) UpdateQSO(q qso.QSO) error {
+ if a.qso == nil {
+ return fmt.Errorf("db not initialized")
+ }
+ return a.qso.Update(a.ctx, q)
+}
+
+func (a *App) DeleteQSO(id int64) error {
+ if a.qso == nil {
+ return fmt.Errorf("db not initialized")
+ }
+ return a.qso.Delete(a.ctx, id)
+}
+
+// WorkedBefore returns prior contacts with the given callsign at both
+// call and DXCC granularity. Pass dxccHint=0 when unknown — the function
+// will infer it from past QSOs with the same call when possible.
+func (a *App) WorkedBefore(callsign string, dxccHint int) (qso.WorkedBefore, error) {
+ if a.qso == nil {
+ return qso.WorkedBefore{}, fmt.Errorf("db not initialized")
+ }
+ return a.qso.WorkedBefore(a.ctx, callsign, dxccHint)
+}
+
+// SetCompactMode toggles a tiny always-on-top window that exposes just the
+// QSO entry — useful when running on a single screen alongside WSJT-X,
+// JT-Alert or the cluster.
+//
+// We can't easily spawn a real second OS window in Wails v2, but a resized
+// always-on-top main window does the job from the user's perspective.
+// Sizes tuned so the compact entry strip fits in a single row (no wrap).
+// Min size must be reduced BEFORE resizing down, otherwise the OS clamps to
+// the previous (larger) min — and increased BEFORE resizing up.
+const (
+ compactW, compactH = 980, 140
+ normalW, normalH = 1400, 900
+ normalMinW, normalMinH = 1100, 700
+)
+
+func (a *App) SetCompactMode(on bool) {
+ if a.ctx == nil {
+ return
+ }
+ if on {
+ wruntime.WindowSetMinSize(a.ctx, compactW, compactH)
+ wruntime.WindowSetSize(a.ctx, compactW, compactH)
+ wruntime.WindowSetAlwaysOnTop(a.ctx, true)
+ } else {
+ wruntime.WindowSetAlwaysOnTop(a.ctx, false)
+ wruntime.WindowSetMinSize(a.ctx, normalMinW, normalMinH)
+ wruntime.WindowSetSize(a.ctx, normalW, normalH)
+ }
+}
+
+// DeleteAllQSO wipes every QSO. Returns the number of rows removed.
+// The frontend MUST gate this behind a strong confirmation prompt.
+func (a *App) DeleteAllQSO() (int64, error) {
+ if a.qso == nil {
+ return 0, fmt.Errorf("db not initialized")
+ }
+ return a.qso.DeleteAll(a.ctx)
+}
+
+// --- ADIF bindings ---
+
+func (a *App) OpenADIFFile() (string, error) {
+ return wruntime.OpenFileDialog(a.ctx, wruntime.OpenDialogOptions{
+ Title: "Import ADIF",
+ Filters: []wruntime.FileFilter{
+ {DisplayName: "ADIF files (*.adi, *.adif)", Pattern: "*.adi;*.adif"},
+ {DisplayName: "All files (*.*)", Pattern: "*.*"},
+ },
+ })
+}
+
+func (a *App) ImportADIF(path string) (adif.ImportResult, error) {
+ if a.qso == nil {
+ return adif.ImportResult{}, fmt.Errorf("db not initialized")
+ }
+ if path == "" {
+ return adif.ImportResult{}, fmt.Errorf("empty path")
+ }
+ im := &adif.Importer{Repo: a.qso}
+ return im.ImportFile(a.ctx, path)
+}
+
+// --- Lookup bindings ---
+
+// LookupCallsign returns the cached or freshly-fetched info for a callsign.
+// Errors are returned as-is to the frontend; ErrNotFound surfaces as
+// "callsign not found".
+func (a *App) LookupCallsign(callsign string) (lookup.Result, error) {
+ if a.lookup == nil {
+ return lookup.Result{}, fmt.Errorf("lookup not initialized")
+ }
+ r, err := a.lookup.Lookup(a.ctx, callsign)
+ if errors.Is(err, lookup.ErrNotFound) {
+ return lookup.Result{}, fmt.Errorf("callsign not found")
+ }
+ return r, err
+}
+
+// GetLookupSettings returns current credentials and cache TTL.
+func (a *App) GetLookupSettings() (LookupSettings, error) {
+ if a.settings == nil {
+ return LookupSettings{}, fmt.Errorf("db not initialized")
+ }
+ m, err := a.settings.GetMany(a.ctx,
+ keyQRZUser, keyQRZPassword, keyHQUser, keyHQPassword,
+ keyCacheTTL, keyLookupPrimary, keyLookupFailsafe)
+ if err != nil {
+ return LookupSettings{}, err
+ }
+ ttl, _ := strconv.Atoi(m[keyCacheTTL])
+ if ttl <= 0 {
+ ttl = 30
+ }
+ return LookupSettings{
+ QRZUser: m[keyQRZUser],
+ QRZPassword: m[keyQRZPassword],
+ HamQTHUser: m[keyHQUser],
+ HamQTHPassword: m[keyHQPassword],
+ Primary: m[keyLookupPrimary],
+ Failsafe: m[keyLookupFailsafe],
+ CacheTTLDays: ttl,
+ }, nil
+}
+
+// SaveLookupSettings persists credentials and rebuilds the provider chain.
+func (a *App) SaveLookupSettings(s LookupSettings) error {
+ if a.settings == nil {
+ return fmt.Errorf("db not initialized")
+ }
+ if s.CacheTTLDays <= 0 {
+ s.CacheTTLDays = 30
+ }
+ // Reject a primary == failsafe routing combo — would just hit the same
+ // provider twice. Frontend should prevent this but defend in depth.
+ if s.Primary != "" && s.Primary == s.Failsafe {
+ s.Failsafe = ""
+ }
+ for k, v := range map[string]string{
+ keyQRZUser: s.QRZUser,
+ keyQRZPassword: s.QRZPassword,
+ keyHQUser: s.HamQTHUser,
+ keyHQPassword: s.HamQTHPassword,
+ keyCacheTTL: strconv.Itoa(s.CacheTTLDays),
+ keyLookupPrimary: s.Primary,
+ keyLookupFailsafe: s.Failsafe,
+ } {
+ if err := a.settings.Set(a.ctx, k, v); err != nil {
+ return err
+ }
+ }
+ a.reloadLookupProviders()
+ return nil
+}
+
+// TestLookupProvider runs a one-shot lookup against a specific provider so
+// the user can verify credentials before saving. callsign defaults to the
+// active profile's callsign when empty (handy "test against my own call").
+// Returns the result on success or a descriptive error.
+func (a *App) TestLookupProvider(name, callsign, user, password string) (lookup.Result, error) {
+ if user == "" || password == "" {
+ return lookup.Result{}, fmt.Errorf("user and password required")
+ }
+ if callsign == "" {
+ if a.profiles != nil {
+ if p, err := a.profiles.Active(a.ctx); err == nil {
+ callsign = p.Callsign
+ }
+ }
+ if callsign == "" {
+ callsign = "W1AW" // ARRL HQ — always present in every database
+ }
+ }
+ var p lookup.Provider
+ switch name {
+ case "qrz":
+ p = lookup.NewQRZ(user, password)
+ case "hamqth":
+ p = lookup.NewHamQTH(user, password)
+ default:
+ return lookup.Result{}, fmt.Errorf("unknown provider %q", name)
+ }
+ r, err := p.Lookup(a.ctx, callsign)
+ if errors.Is(err, lookup.ErrNotFound) {
+ return lookup.Result{}, fmt.Errorf("%s reachable but %q not found (creds look OK)", name, callsign)
+ }
+ if err != nil {
+ return lookup.Result{}, err
+ }
+ r.Source = name
+ return r, nil
+}
+
+// --- CAT bindings ---
+
+// GetCATSettings returns the stored CAT configuration (defaults applied).
+func (a *App) GetCATSettings() (CATSettings, error) {
+ if a.settings == nil {
+ return CATSettings{Backend: "omnirig", OmniRigNum: 1, PollMs: 250}, fmt.Errorf("db not initialized")
+ }
+ m, err := a.settings.GetMany(a.ctx, keyCATEnabled, keyCATBackend, keyCATOmniRigNum, keyCATPollMs, keyCATDelayMs, keyCATDigitalDefault)
+ if err != nil {
+ return CATSettings{}, err
+ }
+ out := CATSettings{
+ Enabled: m[keyCATEnabled] == "1",
+ Backend: m[keyCATBackend],
+ OmniRigNum: 1,
+ PollMs: 250,
+ DelayMs: 0,
+ DigitalDefault: m[keyCATDigitalDefault],
+ }
+ if out.Backend == "" {
+ out.Backend = "omnirig"
+ }
+ if out.DigitalDefault == "" {
+ out.DigitalDefault = "FT8"
+ }
+ if n, _ := strconv.Atoi(m[keyCATOmniRigNum]); n == 1 || n == 2 {
+ out.OmniRigNum = n
+ }
+ if n, _ := strconv.Atoi(m[keyCATPollMs]); n >= 50 && n <= 2000 {
+ out.PollMs = n
+ }
+ if n, _ := strconv.Atoi(m[keyCATDelayMs]); n >= 0 && n <= 500 {
+ out.DelayMs = n
+ }
+ return out, nil
+}
+
+// SaveCATSettings persists CAT config and restarts the manager accordingly.
+func (a *App) SaveCATSettings(s CATSettings) error {
+ if a.settings == nil {
+ return fmt.Errorf("db not initialized")
+ }
+ if s.Backend == "" {
+ s.Backend = "omnirig"
+ }
+ if s.OmniRigNum != 1 && s.OmniRigNum != 2 {
+ s.OmniRigNum = 1
+ }
+ if s.PollMs < 50 || s.PollMs > 2000 {
+ s.PollMs = 250
+ }
+ if s.DelayMs < 0 || s.DelayMs > 500 {
+ s.DelayMs = 0
+ }
+ enabled := "0"
+ if s.Enabled {
+ enabled = "1"
+ }
+ if s.DigitalDefault == "" {
+ s.DigitalDefault = "FT8"
+ }
+ for k, v := range map[string]string{
+ keyCATEnabled: enabled,
+ keyCATBackend: s.Backend,
+ keyCATOmniRigNum: strconv.Itoa(s.OmniRigNum),
+ keyCATPollMs: strconv.Itoa(s.PollMs),
+ keyCATDelayMs: strconv.Itoa(s.DelayMs),
+ keyCATDigitalDefault: strings.ToUpper(strings.TrimSpace(s.DigitalDefault)),
+ } {
+ if err := a.settings.Set(a.ctx, k, v); err != nil {
+ return err
+ }
+ }
+ a.reloadCAT()
+ return nil
+}
+
+// GetCATState returns the current snapshot from the CAT manager. Used by the
+// frontend on mount before any cat:state event has been emitted.
+func (a *App) GetCATState() cat.RigState {
+ if a.cat == nil {
+ return cat.RigState{}
+ }
+ return a.cat.State()
+}
+
+// SetCATFrequency lets the frontend push a freq to the rig (cluster click,
+// memory recall, …). Returns an error if CAT isn't running or the backend
+// refuses (out-of-range, etc.).
+func (a *App) SetCATFrequency(hz int64) error {
+ if a.cat == nil {
+ return fmt.Errorf("cat not initialized")
+ }
+ return a.cat.SetFrequency(hz)
+}
+
+// SetCATMode sets the rig's mode. ADIF mode names (SSB / CW / FT8 / …) are
+// translated to backend-specific values by the backend itself.
+func (a *App) SetCATMode(mode string) error {
+ if a.cat == nil {
+ return fmt.Errorf("cat not initialized")
+ }
+ return a.cat.SetMode(mode)
+}
+
+// SwitchCATRig hot-swaps the active OmniRig slot (Rig1 ↔ Rig2) without
+// requiring a trip through the full Settings panel. Persists the choice
+// so it survives restart.
+func (a *App) SwitchCATRig(n int) error {
+ if n != 1 && n != 2 {
+ return fmt.Errorf("rig num must be 1 or 2, got %d", n)
+ }
+ if a.settings == nil {
+ return fmt.Errorf("db not initialized")
+ }
+ if err := a.settings.Set(a.ctx, keyCATOmniRigNum, strconv.Itoa(n)); err != nil {
+ return err
+ }
+ a.reloadCAT()
+ return nil
+}
+
+// reloadCAT (re)starts the CAT manager based on the current settings.
+// Called at startup and after the user saves new CAT config.
+func (a *App) reloadCAT() {
+ if a.cat == nil {
+ return
+ }
+ s, err := a.GetCATSettings()
+ if err != nil {
+ return
+ }
+ a.cat.SetPollInterval(time.Duration(s.PollMs) * time.Millisecond)
+ a.cat.SetCommandDelay(time.Duration(s.DelayMs) * time.Millisecond)
+ if !s.Enabled {
+ a.cat.Stop()
+ return
+ }
+ switch s.Backend {
+ case "omnirig":
+ // No explicit launch — COM auto-activates OmniRig.exe via its
+ // LocalServer32 registration when we CreateObject in Connect().
+ // Spawning OmniRig.exe ourselves (even with /Embedding) on every
+ // reloadCAT raised the existing instance's window to the front,
+ // which is what Log4OM avoids by relying entirely on COM activation.
+ a.cat.Start(cat.NewOmniRig(s.OmniRigNum))
+ default:
+ // Unknown backend → stop and emit a dummy state so the UI shows it.
+ a.cat.Stop()
+ }
+}
+
+// ClearLookupCache empties the local callsign cache.
+func (a *App) ClearLookupCache() error {
+ if a.cache == nil {
+ return fmt.Errorf("cache not initialized")
+ }
+ return a.cache.Clear(a.ctx)
+}
+
+// CtyDatInfo describes the currently-loaded cty.dat file (or zero values
+// if it hasn't been loaded yet). Exposed for the Maintenance menu so the
+// user can see what they're working with before triggering a refresh.
+type CtyDatInfo struct {
+ Path string `json:"path"`
+ Entities int `json:"entities"`
+ LoadedAt string `json:"loaded_at,omitempty"` // RFC3339, "" if not loaded
+ FileModTime string `json:"file_mod_time,omitempty"` // RFC3339, "" if missing
+}
+
+// GetCtyDatInfo returns metadata about the on-disk cty.dat.
+func (a *App) GetCtyDatInfo() CtyDatInfo {
+ if a.dxcc == nil {
+ return CtyDatInfo{}
+ }
+ src := a.dxcc.Info()
+ out := CtyDatInfo{Path: src.Path, Entities: src.Entities}
+ if !src.LoadedAt.IsZero() {
+ out.LoadedAt = src.LoadedAt.UTC().Format(time.RFC3339)
+ }
+ if !src.FileModTime.IsZero() {
+ out.FileModTime = src.FileModTime.UTC().Format(time.RFC3339)
+ }
+ return out
+}
+
+// RefreshCtyDat re-downloads cty.dat from country-files.com and reloads it
+// into memory. Synchronous so the UI can show a spinner; ~1s typical.
+func (a *App) RefreshCtyDat() (CtyDatInfo, error) {
+ if a.dxcc == nil {
+ return CtyDatInfo{}, fmt.Errorf("dxcc manager not initialized")
+ }
+ if err := a.dxcc.Refresh(a.ctx); err != nil {
+ return CtyDatInfo{}, err
+ }
+ return a.GetCtyDatInfo(), nil
+}
+
+// --- Station bindings ---
+//
+// GetStationSettings/SaveStationSettings now operate on the **currently
+// active profile** rather than a flat settings key set. Kept for the
+// existing topbar/quick-edit code paths; the full profile CRUD lives in
+// the Profile bindings below.
+
+func (a *App) GetStationSettings() (StationSettings, error) {
+ if a.profiles == nil {
+ return StationSettings{}, fmt.Errorf("profiles not initialized")
+ }
+ p, err := a.profiles.Active(a.ctx)
+ if err != nil {
+ return StationSettings{}, err
+ }
+ return StationSettings{
+ Callsign: p.Callsign,
+ Operator: p.Operator,
+ MyGrid: p.MyGrid,
+ MyCountry: p.MyCountry,
+ MySOTARef: p.MySOTARef,
+ MyPOTARef: p.MyPOTARef,
+ }, nil
+}
+
+// --- Lists bindings (bands + modes with default RST) ---
+
+// GetListsSettings returns the user-customisable lists. Defaults are
+// returned when the user has not customised anything.
+func (a *App) GetListsSettings() (ListsSettings, error) {
+ if a.settings == nil {
+ return ListsSettings{Bands: defaultBands, Modes: defaultModes}, fmt.Errorf("db not initialized")
+ }
+ out := ListsSettings{}
+ if raw, _ := a.settings.Get(a.ctx, keyListsBands); raw != "" {
+ _ = json.Unmarshal([]byte(raw), &out.Bands)
+ }
+ if raw, _ := a.settings.Get(a.ctx, keyListsModes); raw != "" {
+ _ = json.Unmarshal([]byte(raw), &out.Modes)
+ }
+ if len(out.Bands) == 0 {
+ out.Bands = append([]string(nil), defaultBands...)
+ }
+ if len(out.Modes) == 0 {
+ out.Modes = append([]ModePreset(nil), defaultModes...)
+ }
+ return out, nil
+}
+
+// SaveListsSettings persists the user-customised lists.
+func (a *App) SaveListsSettings(l ListsSettings) error {
+ if a.settings == nil {
+ return fmt.Errorf("db not initialized")
+ }
+ b, err := json.Marshal(l.Bands)
+ if err != nil {
+ return err
+ }
+ if err := a.settings.Set(a.ctx, keyListsBands, string(b)); err != nil {
+ return err
+ }
+ m, err := json.Marshal(l.Modes)
+ if err != nil {
+ return err
+ }
+ return a.settings.Set(a.ctx, keyListsModes, string(m))
+}
+
+// SaveStationSettings updates only the six "basic" fields on the active
+// profile. Use the Profile bindings (ListProfiles / SaveProfile…) for
+// full multi-profile management.
+func (a *App) SaveStationSettings(s StationSettings) error {
+ if a.profiles == nil {
+ return fmt.Errorf("profiles not initialized")
+ }
+ p, err := a.profiles.Active(a.ctx)
+ if err != nil {
+ return err
+ }
+ p.Callsign = s.Callsign
+ p.Operator = s.Operator
+ p.MyGrid = s.MyGrid
+ p.MyCountry = s.MyCountry
+ p.MySOTARef = s.MySOTARef
+ p.MyPOTARef = s.MyPOTARef
+ return a.profiles.Save(a.ctx, &p)
+}
+
+// --- Profile bindings (multi-profile CRUD) ---
+
+// ListProfiles returns every saved profile, active first.
+func (a *App) ListProfiles() ([]profile.Profile, error) {
+ if a.profiles == nil {
+ return nil, fmt.Errorf("profiles not initialized")
+ }
+ return a.profiles.List(a.ctx)
+}
+
+// GetActiveProfile returns the currently-selected profile.
+func (a *App) GetActiveProfile() (profile.Profile, error) {
+ if a.profiles == nil {
+ return profile.Profile{}, fmt.Errorf("profiles not initialized")
+ }
+ return a.profiles.Active(a.ctx)
+}
+
+// SaveProfile upserts a profile. Pass id=0 to create a new one.
+func (a *App) SaveProfile(p profile.Profile) (profile.Profile, error) {
+ if a.profiles == nil {
+ return profile.Profile{}, fmt.Errorf("profiles not initialized")
+ }
+ if err := a.profiles.Save(a.ctx, &p); err != nil {
+ return profile.Profile{}, err
+ }
+ return p, nil
+}
+
+// DeleteProfile removes a profile. Refuses to delete the last remaining
+// profile; promotes another to active if the deleted one was selected.
+func (a *App) DeleteProfile(id int64) error {
+ if a.profiles == nil {
+ return fmt.Errorf("profiles not initialized")
+ }
+ return a.profiles.Delete(a.ctx, id)
+}
+
+// ActivateProfile switches the selected profile. Subsequent QSOs stamp
+// MY_* fields from this one.
+func (a *App) ActivateProfile(id int64) error {
+ if a.profiles == nil {
+ return fmt.Errorf("profiles not initialized")
+ }
+ return a.profiles.SetActive(a.ctx, id)
+}
+
+// DuplicateProfile clones an existing profile under newName. Useful when
+// the user has a "Home" profile and wants to derive "Portable" from it
+// without retyping every field.
+func (a *App) DuplicateProfile(id int64, newName string) (profile.Profile, error) {
+ if a.profiles == nil {
+ return profile.Profile{}, fmt.Errorf("profiles not initialized")
+ }
+ return a.profiles.Duplicate(a.ctx, id, newName)
+}
diff --git a/build/README.md b/build/README.md
new file mode 100644
index 0000000..1ae2f67
--- /dev/null
+++ b/build/README.md
@@ -0,0 +1,35 @@
+# Build Directory
+
+The build directory is used to house all the build files and assets for your application.
+
+The structure is:
+
+* bin - Output directory
+* darwin - macOS specific files
+* windows - Windows specific files
+
+## Mac
+
+The `darwin` directory holds files specific to Mac builds.
+These may be customised and used as part of the build. To return these files to the default state, simply delete them
+and
+build with `wails build`.
+
+The directory contains the following files:
+
+- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
+- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
+
+## Windows
+
+The `windows` directory contains the manifest and rc files used when building with `wails build`.
+These may be customised for your application. To return these files to the default state, simply delete them and
+build with `wails build`.
+
+- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
+ use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
+ will be created using the `appicon.png` file in the build directory.
+- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
+- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
+ as well as the application itself (right click the exe -> properties -> details)
+- `wails.exe.manifest` - The main application manifest file.
\ No newline at end of file
diff --git a/build/appicon.png b/build/appicon.png
new file mode 100644
index 0000000..63617fe
Binary files /dev/null and b/build/appicon.png differ
diff --git a/build/darwin/Info.dev.plist b/build/darwin/Info.dev.plist
new file mode 100644
index 0000000..14121ef
--- /dev/null
+++ b/build/darwin/Info.dev.plist
@@ -0,0 +1,68 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ {{.Info.ProductName}}
+ CFBundleExecutable
+ {{.OutputFilename}}
+ CFBundleIdentifier
+ com.wails.{{.Name}}
+ CFBundleVersion
+ {{.Info.ProductVersion}}
+ CFBundleGetInfoString
+ {{.Info.Comments}}
+ CFBundleShortVersionString
+ {{.Info.ProductVersion}}
+ CFBundleIconFile
+ iconfile
+ LSMinimumSystemVersion
+ 10.13.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ {{.Info.Copyright}}
+ {{if .Info.FileAssociations}}
+ CFBundleDocumentTypes
+
+ {{range .Info.FileAssociations}}
+
+ CFBundleTypeExtensions
+
+ {{.Ext}}
+
+ CFBundleTypeName
+ {{.Name}}
+ CFBundleTypeRole
+ {{.Role}}
+ CFBundleTypeIconFile
+ {{.IconName}}
+
+ {{end}}
+
+ {{end}}
+ {{if .Info.Protocols}}
+ CFBundleURLTypes
+
+ {{range .Info.Protocols}}
+
+ CFBundleURLName
+ com.wails.{{.Scheme}}
+ CFBundleURLSchemes
+
+ {{.Scheme}}
+
+ CFBundleTypeRole
+ {{.Role}}
+
+ {{end}}
+
+ {{end}}
+ NSAppTransportSecurity
+
+ NSAllowsLocalNetworking
+
+
+
+
diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist
new file mode 100644
index 0000000..d17a747
--- /dev/null
+++ b/build/darwin/Info.plist
@@ -0,0 +1,63 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ {{.Info.ProductName}}
+ CFBundleExecutable
+ {{.OutputFilename}}
+ CFBundleIdentifier
+ com.wails.{{.Name}}
+ CFBundleVersion
+ {{.Info.ProductVersion}}
+ CFBundleGetInfoString
+ {{.Info.Comments}}
+ CFBundleShortVersionString
+ {{.Info.ProductVersion}}
+ CFBundleIconFile
+ iconfile
+ LSMinimumSystemVersion
+ 10.13.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ {{.Info.Copyright}}
+ {{if .Info.FileAssociations}}
+ CFBundleDocumentTypes
+
+ {{range .Info.FileAssociations}}
+
+ CFBundleTypeExtensions
+
+ {{.Ext}}
+
+ CFBundleTypeName
+ {{.Name}}
+ CFBundleTypeRole
+ {{.Role}}
+ CFBundleTypeIconFile
+ {{.IconName}}
+
+ {{end}}
+
+ {{end}}
+ {{if .Info.Protocols}}
+ CFBundleURLTypes
+
+ {{range .Info.Protocols}}
+
+ CFBundleURLName
+ com.wails.{{.Scheme}}
+ CFBundleURLSchemes
+
+ {{.Scheme}}
+
+ CFBundleTypeRole
+ {{.Role}}
+
+ {{end}}
+
+ {{end}}
+
+
diff --git a/build/windows/icon.ico b/build/windows/icon.ico
new file mode 100644
index 0000000..f334798
Binary files /dev/null and b/build/windows/icon.ico differ
diff --git a/build/windows/info.json b/build/windows/info.json
new file mode 100644
index 0000000..9727946
--- /dev/null
+++ b/build/windows/info.json
@@ -0,0 +1,15 @@
+{
+ "fixed": {
+ "file_version": "{{.Info.ProductVersion}}"
+ },
+ "info": {
+ "0000": {
+ "ProductVersion": "{{.Info.ProductVersion}}",
+ "CompanyName": "{{.Info.CompanyName}}",
+ "FileDescription": "{{.Info.ProductName}}",
+ "LegalCopyright": "{{.Info.Copyright}}",
+ "ProductName": "{{.Info.ProductName}}",
+ "Comments": "{{.Info.Comments}}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/build/windows/installer/project.nsi b/build/windows/installer/project.nsi
new file mode 100644
index 0000000..654ae2e
--- /dev/null
+++ b/build/windows/installer/project.nsi
@@ -0,0 +1,114 @@
+Unicode true
+
+####
+## Please note: Template replacements don't work in this file. They are provided with default defines like
+## mentioned underneath.
+## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
+## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
+## from outside of Wails for debugging and development of the installer.
+##
+## For development first make a wails nsis build to populate the "wails_tools.nsh":
+## > wails build --target windows/amd64 --nsis
+## Then you can call makensis on this file with specifying the path to your binary:
+## For a AMD64 only installer:
+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
+## For a ARM64 only installer:
+## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
+## For a installer with both architectures:
+## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
+####
+## The following information is taken from the ProjectInfo file, but they can be overwritten here.
+####
+## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
+## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
+## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
+## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
+## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
+###
+## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
+## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
+####
+## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
+####
+## Include the wails tools
+####
+!include "wails_tools.nsh"
+
+# The version information for this two must consist of 4 parts
+VIProductVersion "${INFO_PRODUCTVERSION}.0"
+VIFileVersion "${INFO_PRODUCTVERSION}.0"
+
+VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
+VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
+VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
+VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
+VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
+VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
+
+# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
+ManifestDPIAware true
+
+!include "MUI.nsh"
+
+!define MUI_ICON "..\icon.ico"
+!define MUI_UNICON "..\icon.ico"
+# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
+!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
+!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
+
+!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
+# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
+!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
+!insertmacro MUI_PAGE_INSTFILES # Installing page.
+!insertmacro MUI_PAGE_FINISH # Finished installation page.
+
+!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
+
+!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
+
+## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
+#!uninstfinalize 'signtool --file "%1"'
+#!finalize 'signtool --file "%1"'
+
+Name "${INFO_PRODUCTNAME}"
+OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
+InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
+ShowInstDetails show # This will always show the installation details.
+
+Function .onInit
+ !insertmacro wails.checkArchitecture
+FunctionEnd
+
+Section
+ !insertmacro wails.setShellContext
+
+ !insertmacro wails.webview2runtime
+
+ SetOutPath $INSTDIR
+
+ !insertmacro wails.files
+
+ CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+ CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+
+ !insertmacro wails.associateFiles
+ !insertmacro wails.associateCustomProtocols
+
+ !insertmacro wails.writeUninstaller
+SectionEnd
+
+Section "uninstall"
+ !insertmacro wails.setShellContext
+
+ RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
+
+ RMDir /r $INSTDIR
+
+ Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
+ Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
+
+ !insertmacro wails.unassociateFiles
+ !insertmacro wails.unassociateCustomProtocols
+
+ !insertmacro wails.deleteUninstaller
+SectionEnd
diff --git a/build/windows/installer/wails_tools.nsh b/build/windows/installer/wails_tools.nsh
new file mode 100644
index 0000000..2f6d321
--- /dev/null
+++ b/build/windows/installer/wails_tools.nsh
@@ -0,0 +1,249 @@
+# DO NOT EDIT - Generated automatically by `wails build`
+
+!include "x64.nsh"
+!include "WinVer.nsh"
+!include "FileFunc.nsh"
+
+!ifndef INFO_PROJECTNAME
+ !define INFO_PROJECTNAME "{{.Name}}"
+!endif
+!ifndef INFO_COMPANYNAME
+ !define INFO_COMPANYNAME "{{.Info.CompanyName}}"
+!endif
+!ifndef INFO_PRODUCTNAME
+ !define INFO_PRODUCTNAME "{{.Info.ProductName}}"
+!endif
+!ifndef INFO_PRODUCTVERSION
+ !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
+!endif
+!ifndef INFO_COPYRIGHT
+ !define INFO_COPYRIGHT "{{.Info.Copyright}}"
+!endif
+!ifndef PRODUCT_EXECUTABLE
+ !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
+!endif
+!ifndef UNINST_KEY_NAME
+ !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
+!endif
+!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
+
+!ifndef REQUEST_EXECUTION_LEVEL
+ !define REQUEST_EXECUTION_LEVEL "admin"
+!endif
+
+RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
+
+!ifdef ARG_WAILS_AMD64_BINARY
+ !define SUPPORTS_AMD64
+!endif
+
+!ifdef ARG_WAILS_ARM64_BINARY
+ !define SUPPORTS_ARM64
+!endif
+
+!ifdef SUPPORTS_AMD64
+ !ifdef SUPPORTS_ARM64
+ !define ARCH "amd64_arm64"
+ !else
+ !define ARCH "amd64"
+ !endif
+!else
+ !ifdef SUPPORTS_ARM64
+ !define ARCH "arm64"
+ !else
+ !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
+ !endif
+!endif
+
+!macro wails.checkArchitecture
+ !ifndef WAILS_WIN10_REQUIRED
+ !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
+ !endif
+
+ !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
+ !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
+ !endif
+
+ ${If} ${AtLeastWin10}
+ !ifdef SUPPORTS_AMD64
+ ${if} ${IsNativeAMD64}
+ Goto ok
+ ${EndIf}
+ !endif
+
+ !ifdef SUPPORTS_ARM64
+ ${if} ${IsNativeARM64}
+ Goto ok
+ ${EndIf}
+ !endif
+
+ IfSilent silentArch notSilentArch
+ silentArch:
+ SetErrorLevel 65
+ Abort
+ notSilentArch:
+ MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
+ Quit
+ ${else}
+ IfSilent silentWin notSilentWin
+ silentWin:
+ SetErrorLevel 64
+ Abort
+ notSilentWin:
+ MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
+ Quit
+ ${EndIf}
+
+ ok:
+!macroend
+
+!macro wails.files
+ !ifdef SUPPORTS_AMD64
+ ${if} ${IsNativeAMD64}
+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
+ ${EndIf}
+ !endif
+
+ !ifdef SUPPORTS_ARM64
+ ${if} ${IsNativeARM64}
+ File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
+ ${EndIf}
+ !endif
+!macroend
+
+!macro wails.writeUninstaller
+ WriteUninstaller "$INSTDIR\uninstall.exe"
+
+ SetRegView 64
+ WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
+ WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
+ WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
+ WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
+
+ ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
+ IntFmt $0 "0x%08X" $0
+ WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
+!macroend
+
+!macro wails.deleteUninstaller
+ Delete "$INSTDIR\uninstall.exe"
+
+ SetRegView 64
+ DeleteRegKey HKLM "${UNINST_KEY}"
+!macroend
+
+!macro wails.setShellContext
+ ${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
+ SetShellVarContext all
+ ${else}
+ SetShellVarContext current
+ ${EndIf}
+!macroend
+
+# Install webview2 by launching the bootstrapper
+# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
+!macro wails.webview2runtime
+ !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
+ !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
+ !endif
+
+ SetRegView 64
+ # If the admin key exists and is not empty then webview2 is already installed
+ ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
+ ${If} $0 != ""
+ Goto ok
+ ${EndIf}
+
+ ${If} ${REQUEST_EXECUTION_LEVEL} == "user"
+ # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
+ ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
+ ${If} $0 != ""
+ Goto ok
+ ${EndIf}
+ ${EndIf}
+
+ SetDetailsPrint both
+ DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
+ SetDetailsPrint listonly
+
+ InitPluginsDir
+ CreateDirectory "$pluginsdir\webview2bootstrapper"
+ SetOutPath "$pluginsdir\webview2bootstrapper"
+ File "tmp\MicrosoftEdgeWebview2Setup.exe"
+ ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
+
+ SetDetailsPrint both
+ ok:
+!macroend
+
+# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
+!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
+ ; Backup the previously associated file class
+ ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
+
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
+
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
+!macroend
+
+!macro APP_UNASSOCIATE EXT FILECLASS
+ ; Backup the previously associated file class
+ ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
+ WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
+
+ DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
+!macroend
+
+!macro wails.associateFiles
+ ; Create file associations
+ {{range .Info.FileAssociations}}
+ !insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
+
+ File "..\{{.IconName}}.ico"
+ {{end}}
+!macroend
+
+!macro wails.unassociateFiles
+ ; Delete app associations
+ {{range .Info.FileAssociations}}
+ !insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
+
+ Delete "$INSTDIR\{{.IconName}}.ico"
+ {{end}}
+!macroend
+
+!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
+ DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
+ WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
+!macroend
+
+!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
+ DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
+!macroend
+
+!macro wails.associateCustomProtocols
+ ; Create custom protocols associations
+ {{range .Info.Protocols}}
+ !insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
+
+ {{end}}
+!macroend
+
+!macro wails.unassociateCustomProtocols
+ ; Delete app custom protocol associations
+ {{range .Info.Protocols}}
+ !insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
+ {{end}}
+!macroend
diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest
new file mode 100644
index 0000000..17e1a23
--- /dev/null
+++ b/build/windows/wails.exe.manifest
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ true/pm
+ permonitorv2,permonitor
+
+
+
\ No newline at end of file
diff --git a/cmd/dbdiag/main.go b/cmd/dbdiag/main.go
new file mode 100644
index 0000000..1c9066b
--- /dev/null
+++ b/cmd/dbdiag/main.go
@@ -0,0 +1,172 @@
+// dbdiag inspects HamLog DB state. Optional 2nd arg: "wb CALL DXCC".
+package main
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+
+ _ "modernc.org/sqlite"
+)
+
+const migrationsDir = "internal/db/migrations"
+
+func main() {
+ base, _ := os.UserConfigDir()
+ dbPath := filepath.Join(base, "HamLog", "hamlog.db")
+
+ mode := "summary"
+ var call string
+ var dxcc int
+ if len(os.Args) >= 3 && os.Args[1] == "wb" {
+ mode = "wb"
+ call = strings.ToUpper(os.Args[2])
+ if len(os.Args) >= 4 {
+ dxcc, _ = strconv.Atoi(os.Args[3])
+ }
+ }
+
+ fmt.Println("DB path:", dbPath)
+ dsn := "file:" + dbPath + "?_pragma=foreign_keys(on)"
+ conn, err := sql.Open("sqlite", dsn)
+ must("open", err)
+ defer conn.Close()
+ must("ping", conn.Ping())
+
+ if mode == "wb" {
+ probeWB(conn, call, dxcc)
+ probeCache(conn, call)
+ return
+ }
+ summary(conn)
+}
+
+func probeCache(conn *sql.DB, call string) {
+ fmt.Println("\nCache row for", call, ":")
+ row := conn.QueryRow(`SELECT name, qth, country, grid, dxcc, source, fetched_at
+ FROM callsign_cache WHERE callsign = ?`, call)
+ var name, qth, country, grid, src, fetched sql.NullString
+ var dxcc sql.NullInt64
+ if err := row.Scan(&name, &qth, &country, &grid, &dxcc, &src, &fetched); err != nil {
+ fmt.Println(" (no cache row:", err, ")")
+ return
+ }
+ fmt.Printf(" name=%q qth=%q country=%q grid=%q\n", name.String, qth.String, country.String, grid.String)
+ fmt.Printf(" dxcc=%v(valid=%v) source=%s fetched_at=%s\n", dxcc.Int64, dxcc.Valid, src.String, fetched.String)
+}
+
+func probeWB(conn *sql.DB, call string, dxcc int) {
+ fmt.Printf("\nProbing WorkedBefore for call=%q dxcc=%d\n", call, dxcc)
+
+ var count int
+ must("count call", conn.QueryRow(`SELECT COUNT(*) FROM qso WHERE callsign = ?`, call).Scan(&count))
+ fmt.Printf(" count(callsign=%s) = %d\n", call, count)
+
+ if dxcc == 0 {
+ var d sql.NullInt64
+ _ = conn.QueryRow(`SELECT dxcc FROM qso
+ WHERE callsign = ? AND dxcc IS NOT NULL
+ ORDER BY qso_date DESC LIMIT 1`, call).Scan(&d)
+ if d.Valid {
+ dxcc = int(d.Int64)
+ fmt.Printf(" inferred dxcc from prior QSO: %d\n", dxcc)
+ }
+ }
+ if dxcc == 0 {
+ fmt.Println(" no DXCC available — skipping entity stats")
+ return
+ }
+
+ var dxccCount int
+ must("count dxcc", conn.QueryRow(`SELECT COUNT(*) FROM qso WHERE dxcc = ?`, dxcc).Scan(&dxccCount))
+ fmt.Printf(" count(dxcc=%d) = %d\n", dxcc, dxccCount)
+
+ // Distinct (band, mode) for this DXCC
+ rows, err := conn.Query(`SELECT band, mode, COUNT(*) FROM qso WHERE dxcc = ? GROUP BY band, mode ORDER BY band, mode`, dxcc)
+ must("group by band/mode", err)
+ defer rows.Close()
+ fmt.Printf(" distinct (band, mode) groups for dxcc=%d:\n", dxcc)
+ var n int
+ for rows.Next() {
+ var band, mode string
+ var c int
+ rows.Scan(&band, &mode, &c)
+ fmt.Printf(" %-8s %-12s %d QSOs\n", band, mode, c)
+ n++
+ }
+ if n == 0 {
+ fmt.Println(" (none)")
+ }
+
+ // Same query as backend
+ fmt.Println("\n --- exact backend query ---")
+ rows2, err := conn.Query(`
+ SELECT band, mode,
+ MAX(CASE WHEN callsign = ? THEN 1 ELSE 0 END),
+ MAX(CASE WHEN callsign = ?
+ AND (lotw_rcvd = 'Y' OR qsl_rcvd = 'Y' OR eqsl_rcvd = 'Y')
+ THEN 1 ELSE 0 END),
+ MAX(CASE WHEN lotw_rcvd = 'Y' OR qsl_rcvd = 'Y' OR eqsl_rcvd = 'Y'
+ THEN 1 ELSE 0 END)
+ FROM qso WHERE dxcc = ?
+ GROUP BY band, mode
+ ORDER BY band, mode`, call, call, dxcc)
+ must("backend query", err)
+ defer rows2.Close()
+ n = 0
+ for rows2.Next() {
+ var band, mode string
+ var cw, cc, dc int
+ rows2.Scan(&band, &mode, &cw, &cc, &dc)
+ fmt.Printf(" %-8s %-12s callW=%d callC=%d dxccC=%d\n", band, mode, cw, cc, dc)
+ n++
+ }
+ fmt.Printf(" → %d rows\n", n)
+}
+
+func summary(conn *sql.DB) {
+ if st, err := os.Stat(filepath.Join(filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir("a")))), "")); err == nil {
+ fmt.Printf("(size info skipped) %v\n", st)
+ }
+
+ fmt.Println("Applied migrations:")
+ rows, err := conn.Query(`SELECT name FROM schema_migrations ORDER BY name`)
+ if err == nil {
+ for rows.Next() {
+ var n string
+ rows.Scan(&n)
+ fmt.Println(" -", n)
+ }
+ rows.Close()
+ }
+
+ var n int64
+ conn.QueryRow(`SELECT COUNT(*) FROM qso`).Scan(&n)
+ fmt.Println("\nQSO rows:", n)
+
+ var withDxcc int64
+ conn.QueryRow(`SELECT COUNT(*) FROM qso WHERE dxcc IS NOT NULL`).Scan(&withDxcc)
+ fmt.Printf("Rows with non-null dxcc: %d (%.1f%%)\n", withDxcc, float64(withDxcc)/float64(n)*100)
+
+ var nDistinctDXCC int
+ conn.QueryRow(`SELECT COUNT(DISTINCT dxcc) FROM qso WHERE dxcc IS NOT NULL`).Scan(&nDistinctDXCC)
+ fmt.Println("Distinct DXCC entities in log:", nDistinctDXCC)
+
+ fmt.Println("\nTry: go run ./cmd/dbdiag wb 4X6TT")
+ fmt.Println(" go run ./cmd/dbdiag wb 4X6TT 336")
+ _ = context.Background
+ _ = sort.Strings
+ must("dummy", nil)
+}
+
+func must(label string, err error) {
+ if err != nil {
+ fmt.Fprintln(os.Stderr, label+":", err)
+ os.Exit(1)
+ }
+}
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..aa50b84
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ HamLog
+
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..157abae
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,3774 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.0.0",
+ "dependencies": {
+ "@radix-ui/react-alert-dialog": "^1.1.15",
+ "@radix-ui/react-checkbox": "^1.3.3",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-popover": "^1.1.15",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-tabs": "^1.1.13",
+ "@radix-ui/react-tooltip": "^1.2.8",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^1.16.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "tailwind-merge": "^3.6.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.3.0",
+ "@types/node": "^25.9.1",
+ "@types/react": "^18.0.17",
+ "@types/react-dom": "^18.0.6",
+ "@vitejs/plugin-react": "^4.7.0",
+ "tailwindcss": "^4.3.0",
+ "tw-animate-css": "^1.4.0",
+ "typescript": "^5.9.3",
+ "vite": "^6.4.2"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.3",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
+ "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
+ "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz",
+ "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.6"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@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==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-alert-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",
+ "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dialog": "1.1.15",
+ "@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-alert-dialog/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-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==",
+ "license": "MIT",
+ "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-checkbox": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
+ "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "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==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-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==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-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-focus-guards": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz",
+ "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
+ "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-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-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
+ "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-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",
+ "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-popover/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-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==",
+ "license": "MIT",
+ "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-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
+ "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-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==",
+ "license": "MIT",
+ "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-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-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==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
+ "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+ "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
+ "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-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-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@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==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-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==",
+ "license": "MIT",
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-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==",
+ "license": "MIT",
+ "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/rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
+ "license": "MIT"
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
+ "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
+ "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
+ "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
+ "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
+ "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
+ "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
+ "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
+ "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
+ "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
+ "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
+ "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
+ "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
+ "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
+ "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
+ "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
+ "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
+ "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
+ "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
+ "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
+ "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
+ "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
+ "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
+ "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
+ "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
+ "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz",
+ "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.21.0",
+ "jiti": "^2.6.1",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz",
+ "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.0",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.0",
+ "@tailwindcss/oxide-darwin-x64": "4.3.0",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.0",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.0",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.0",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.0",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.0",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz",
+ "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz",
+ "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz",
+ "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz",
+ "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz",
+ "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz",
+ "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz",
+ "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz",
+ "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz",
+ "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz",
+ "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.10.0",
+ "@emnapi/runtime": "^1.10.0",
+ "@emnapi/wasi-threads": "^1.2.1",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.1",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz",
+ "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz",
+ "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz",
+ "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.3.0",
+ "@tailwindcss/oxide": "4.3.0",
+ "tailwindcss": "4.3.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "25.9.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
+ "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": ">=7.24.0 <7.24.7"
+ }
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.29",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz",
+ "integrity": "sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.32",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz",
+ "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001793",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
+ "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.361",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz",
+ "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.22.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz",
+ "integrity": "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz",
+ "integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.46",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz",
+ "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
+ "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.4",
+ "@rollup/rollup-android-arm64": "4.60.4",
+ "@rollup/rollup-darwin-arm64": "4.60.4",
+ "@rollup/rollup-darwin-x64": "4.60.4",
+ "@rollup/rollup-freebsd-arm64": "4.60.4",
+ "@rollup/rollup-freebsd-x64": "4.60.4",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.4",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.4",
+ "@rollup/rollup-linux-arm64-musl": "4.60.4",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.4",
+ "@rollup/rollup-linux-loong64-musl": "4.60.4",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.4",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.4",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.4",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.4",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.4",
+ "@rollup/rollup-linux-x64-gnu": "4.60.4",
+ "@rollup/rollup-linux-x64-musl": "4.60.4",
+ "@rollup/rollup-openbsd-x64": "4.60.4",
+ "@rollup/rollup-openharmony-arm64": "4.60.4",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.4",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.4",
+ "@rollup/rollup-win32-x64-gnu": "4.60.4",
+ "@rollup/rollup-win32-x64-msvc": "4.60.4",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tailwind-merge": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
+ "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",
+ "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/tw-animate-css": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
+ "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Wombosvideo"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.24.6",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
+ "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..56c2c45
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@radix-ui/react-alert-dialog": "^1.1.15",
+ "@radix-ui/react-checkbox": "^1.3.3",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-popover": "^1.1.15",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-tabs": "^1.1.13",
+ "@radix-ui/react-tooltip": "^1.2.8",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^1.16.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "tailwind-merge": "^3.6.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.3.0",
+ "@types/node": "^25.9.1",
+ "@types/react": "^18.0.17",
+ "@types/react-dom": "^18.0.6",
+ "@vitejs/plugin-react": "^4.7.0",
+ "tailwindcss": "^4.3.0",
+ "tw-animate-css": "^1.4.0",
+ "typescript": "^5.9.3",
+ "vite": "^6.4.2"
+ }
+}
diff --git a/frontend/package.json.md5 b/frontend/package.json.md5
new file mode 100644
index 0000000..32677fe
--- /dev/null
+++ b/frontend/package.json.md5
@@ -0,0 +1 @@
+58f02c99f9fceb8f5aeae2c8b90fd325
\ No newline at end of file
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 0000000..f6ebe03
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,1357 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import {
+ AlertCircle, Antenna, CheckCircle2, Clock, Compass, Hash, Loader2, Lock,
+ Maximize2, Minimize2, Pencil, RadioTower, RefreshCw, Send, Settings, Trash2, Unlock, X,
+} from 'lucide-react';
+
+import {
+ AddQSO, ListQSO, CountQSO,
+ OpenADIFFile, ImportADIF,
+ GetQSO, UpdateQSO, DeleteQSO, DeleteAllQSO,
+ LookupCallsign, GetStationSettings, GetListsSettings,
+ GetStartupStatus,
+ WorkedBefore,
+ SetCompactMode,
+ GetCATState, SetCATFrequency, SetCATMode, SwitchCATRig,
+ RefreshCtyDat,
+ GetCATSettings,
+} from '../wailsjs/go/main/App';
+import { EventsOn, EventsOff } from '../wailsjs/runtime/runtime';
+import type { adif as adifModels, lookup as lookupModels, cat as catModels } from '../wailsjs/go/models';
+import type { QSOForm, WorkedBeforeView, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
+
+import { Menubar, type Menu } from '@/components/Menubar';
+import { ConfirmDialog } from '@/components/ConfirmDialog';
+import { SettingsModal } from '@/components/SettingsModal';
+import { QSOEditModal } from '@/components/QSOEditModal';
+import { BandSlotGrid } from '@/components/BandSlotGrid';
+import { CallHistoryPanel } from '@/components/CallHistoryPanel';
+import { DetailsPanel, type DetailsState } from '@/components/DetailsPanel';
+
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Badge } from '@/components/ui/badge';
+import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
+import {
+ Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
+} from '@/components/ui/select';
+import { cn } from '@/lib/utils';
+import { pathBetween } from '@/lib/maidenhead';
+
+type QSO = QSOForm;
+type ImportResult = adifModels.ImportResult;
+type LookupResult = lookupModels.Result;
+type StationSettings = StationSettingsForm;
+type ListsSettings = ListsSettingsForm;
+type ModePreset = ModePresetForm;
+type WB = WorkedBeforeView;
+type CATState = Omit;
+
+const DEFAULT_BANDS = ['160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','2m','70cm','23cm'];
+const DEFAULT_MODES = ['SSB','CW','FT8','FT4','RTTY','PSK31','AM','FM','DIGITALVOICE'];
+
+const emptyDetails: DetailsState = {
+ state: '', cnty: '', address: '',
+ lat: undefined, lon: undefined,
+ dxcc: undefined, cqz: undefined, ituz: undefined, cont: '',
+ qsl_msg: '', qsl_via: '',
+ ant_az: undefined, ant_el: undefined, ant_path: '',
+ prop_mode: '', my_rig: '', my_antenna: '',
+ tx_pwr: undefined,
+ sat_name: '', sat_mode: '',
+ contest_id: '', srx: undefined, stx: undefined,
+ email: '',
+};
+
+function fmtDateUTC(s: any): string {
+ if (!s) return '';
+ const d = new Date(s);
+ if (isNaN(d.getTime())) return s;
+ const p = (n: number) => String(n).padStart(2, '0');
+ return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`;
+}
+function fmtFreq(hz?: number): string {
+ if (!hz) return '';
+ return (hz / 1_000_000).toFixed(4);
+}
+function fmtHMSUTC(d: Date): string {
+ const p = (n: number) => String(n).padStart(2, '0');
+ return `${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}`;
+}
+// parseHMSUTC parses "HH:MM" or "HH:MM:SS" and returns a Date with that
+// UTC time on the same UTC day as base. Tolerates "HHMM" / "HHMMSS" too
+// (no separators) so the user can type fast. Falls back to base on bad input.
+function parseHMSUTC(s: string, base: Date): Date {
+ const clean = s.replace(/[^0-9:]/g, '');
+ let h = 0, m = 0, sec = 0;
+ if (clean.includes(':')) {
+ const parts = clean.split(':');
+ h = parseInt(parts[0] ?? '0', 10) || 0;
+ m = parseInt(parts[1] ?? '0', 10) || 0;
+ sec = parseInt(parts[2] ?? '0', 10) || 0;
+ } else if (clean.length >= 4) {
+ h = parseInt(clean.slice(0, 2), 10) || 0;
+ m = parseInt(clean.slice(2, 4), 10) || 0;
+ sec = clean.length >= 6 ? (parseInt(clean.slice(4, 6), 10) || 0) : 0;
+ } else {
+ return base;
+ }
+ if (h > 23 || m > 59 || sec > 59) return base;
+ const d = new Date(base);
+ d.setUTCHours(h, m, sec, 0);
+ return d;
+}
+// fmtFreqDots formats a MHz string for display as MHz.kHz.Hz with dot
+// separators (14.266500 → "14.266.500"). Pads the fractional part to 6
+// digits so partial inputs ("14.21") still render as "14.210.000".
+function fmtFreqDots(mhzStr: string): string {
+ if (!mhzStr) return '';
+ const [intPart, fracRaw = ''] = mhzStr.split('.');
+ const frac = (fracRaw + '000000').slice(0, 6);
+ return `${intPart}.${frac.slice(0, 3)}.${frac.slice(3, 6)}`;
+}
+// shortCatError condenses a backend error into a few words for the topbar
+// pill. The full message stays in the tooltip. Recognises the common cases
+// (OmniRig not installed, not registered) and otherwise truncates.
+function shortCatError(err?: string): string {
+ if (!err) return '';
+ const e = err.toLowerCase();
+ if (e.includes('not registered') || e.includes('not available')) return 'OmniRig not found';
+ if (e.includes('not connected')) return 'not connected';
+ if (e.includes('coinitialize')) return 'COM error';
+ return err.length > 24 ? err.slice(0, 22) + '…' : err;
+}
+function computePrefix(call: string): string {
+ if (!call) return '';
+ const c = call.trim().toUpperCase().split('/')[0];
+ let lastDigit = -1;
+ for (let i = 0; i < c.length; i++) {
+ if (c[i] >= '0' && c[i] <= '9') lastDigit = i;
+ }
+ return lastDigit >= 0 ? c.slice(0, lastDigit + 1) : c;
+}
+
+export default function App() {
+ // === Lists from settings (fallback for first paint) ===
+ const [bands, setBands] = useState(DEFAULT_BANDS);
+ const [modes, setModes] = useState(DEFAULT_MODES);
+ const [modePresets, setModePresets] = useState([]);
+
+ // === Entry ===
+ const [callsign, setCallsign] = useState('');
+ // QSO start time — frozen when the operator starts typing the callsign,
+ // logged as ADIF QSO_DATE. End time = save click (logged as QSO_DATE_OFF).
+ const [qsoStartedAt, setQsoStartedAt] = useState(null);
+ // Frozen end time used when the End-UTC field is locked (backdated QSO);
+ // null means the field tracks the live clock.
+ const [qsoEndedAt, setQsoEndedAt] = useState(null);
+ // Local string buffers for the time inputs while the user is editing.
+ // Parsing only on blur — otherwise every keystroke would re-format the
+ // value and React would move the cursor back to the end of the field.
+ const [startInputStr, setStartInputStr] = useState('');
+ const [endInputStr, setEndInputStr] = useState('');
+ const [startFocused, setStartFocused] = useState(false);
+ const [endFocused, setEndFocused] = useState(false);
+ const [freqFocused, setFreqFocused] = useState(false);
+ // Per-field locks — like Log4OM's padlocks. When locked, CAT updates skip
+ // that field and the time fields become editable so the user can log a
+ // QSO from the past while the radio is parked elsewhere.
+ type LockKey = 'band' | 'mode' | 'freq' | 'start' | 'end';
+ const [locks, setLocks] = useState>({
+ band: false, mode: false, freq: false, start: false, end: false,
+ });
+ const locksRef = useRef(locks);
+ useEffect(() => { locksRef.current = locks; }, [locks]);
+ const toggleLock = (k: LockKey) => {
+ setLocks((s) => {
+ const wasLocked = s[k];
+ const next = { ...s, [k]: !wasLocked };
+ // Unlocking → restore automatic behavior. Without this the locked
+ // value would linger forever: a stale Start time would never refresh
+ // even after a new callsign is entered.
+ if (wasLocked) {
+ if (k === 'start') {
+ // If a QSO is currently in progress (callsign typed), snap start
+ // to now since we missed the auto-start moment. Otherwise clear.
+ setQsoStartedAt(callsign.trim() ? new Date() : null);
+ } else if (k === 'end') {
+ // Drop the frozen end so the field tracks the live UTC clock.
+ setQsoEndedAt(null);
+ }
+ }
+ return next;
+ });
+ };
+ // Small padlock toggle rendered inside each lockable field's label. Match
+ // the icon to the current state so the user can tell at a glance which
+ // fields are immune to CAT updates / live clock.
+ function LockBtn({ k, title }: { k: LockKey; title: string }) {
+ const on = locks[k];
+ const Icon = on ? Lock : Unlock;
+ return (
+
+ );
+ }
+ const [band, setBand] = useState('20m');
+ const [mode, setMode] = useState('SSB');
+ const [freqMhz, setFreqMhz] = useState('');
+ // RX freq for split — only set/shown when the rig is in split mode.
+ const [rxFreqMhz, setRxFreqMhz] = useState('');
+ const [rstSent, setRstSent] = useState('59');
+ const [rstRcvd, setRstRcvd] = useState('59');
+ const [grid, setGrid] = useState('');
+ const [name, setName] = useState('');
+ const [qth, setQth] = useState('');
+ const [country, setCountry] = useState('');
+ const [comment, setComment] = useState('');
+ const [note, setNote] = useState('');
+
+ // Compact (popout) mode: shrinks the window + always-on-top so the entry
+ // strip can sit on top of WSJT-X / JT-Alert / the cluster.
+ const [compact, setCompact] = useState(false);
+ function toggleCompact() {
+ const next = !compact;
+ setCompact(next);
+ SetCompactMode(next);
+ }
+
+ // CAT — receives live rig state via Wails events.
+ const [catState, setCatState] = useState({ enabled: false, connected: false } as any);
+ // Mode HamLog shows when the rig reports generic DIG_U/DIG_L. OmniRig
+ // can't tell us if it's FT8 vs FT4 vs RTTY, so the user picks the default
+ // in Preferences > Hardware > CAT interface.
+ const digitalDefaultRef = useRef('FT8');
+ // Don't override freq/band/mode the user JUST typed — track a small grace
+ // window after manual edits and skip CAT updates during it.
+ const catFreezeUntilRef = useRef(0);
+ function noteManualEdit() { catFreezeUntilRef.current = Date.now() + 1500; }
+
+ // Suggested QSY frequency (Hz) for a given band + mode. Common phone /
+ // CW / FT8 watering holes per IARU practice. Fallback = mid-band SSB freq.
+ // TODO: make this a user preference per band/mode later.
+ const QSY_TABLE: Record> = {
+ '160m': { SSB: 1842000, CW: 1820000, FT8: 1840000, FT4: 1840000, RTTY: 1838000, default: 1840000 },
+ '80m': { SSB: 3750000, CW: 3520000, FT8: 3573000, FT4: 3575000, RTTY: 3580000, default: 3750000 },
+ '60m': { default: 5357000 },
+ '40m': { SSB: 7150000, CW: 7030000, FT8: 7074000, FT4: 7047500, RTTY: 7040000, default: 7150000 },
+ '30m': { CW: 10116000, FT8: 10136000, FT4: 10140000, RTTY: 10142000, default: 10136000 },
+ '20m': { SSB: 14250000, CW: 14030000, FT8: 14074000, FT4: 14080000, RTTY: 14080000, default: 14250000 },
+ '17m': { SSB: 18140000, CW: 18080000, FT8: 18100000, FT4: 18104000, RTTY: 18105000, default: 18140000 },
+ '15m': { SSB: 21300000, CW: 21030000, FT8: 21074000, FT4: 21140000, RTTY: 21080000, default: 21300000 },
+ '12m': { SSB: 24950000, CW: 24910000, FT8: 24915000, FT4: 24919000, RTTY: 24920000, default: 24950000 },
+ '10m': { SSB: 28500000, CW: 28030000, FT8: 28074000, FT4: 28180000, RTTY: 28080000, default: 28500000 },
+ '6m': { SSB: 50150000, CW: 50080000, FT8: 50313000, FT4: 50318000, default: 50150000 },
+ '2m': { SSB: 144300000, CW: 144050000, FT8: 144174000, default: 144300000 },
+ '70cm': { SSB: 432300000, CW: 432050000, FT8: 432174000, default: 432300000 },
+ };
+ function qsyFreqHz(band: string, mode: string): number {
+ const t = QSY_TABLE[band];
+ if (!t) return 0;
+ return t[mode] ?? t.default ?? 0;
+ }
+
+ // Digital watering holes (±3 kHz tolerance). When the rig is parked here
+ // CAT typically reports SSB (USB-D under the hood) which is misleading —
+ // auto-promote to the specific digital mode for the entry strip.
+ const DIGITAL_FREQS: { freq: number; mode: string }[] = [
+ { freq: 1.840, mode: 'FT8' },
+ { freq: 3.573, mode: 'FT8' },
+ { freq: 5.357, mode: 'FT8' },
+ { freq: 7.074, mode: 'FT8' }, { freq: 7.0475, mode: 'FT4' },
+ { freq: 10.136, mode: 'FT8' }, { freq: 10.140, mode: 'FT4' },
+ { freq: 14.074, mode: 'FT8' }, { freq: 14.080, mode: 'FT4' },
+ { freq: 18.100, mode: 'FT8' }, { freq: 18.104, mode: 'FT4' },
+ { freq: 21.074, mode: 'FT8' }, { freq: 21.140, mode: 'FT4' },
+ { freq: 24.915, mode: 'FT8' }, { freq: 24.919, mode: 'FT4' },
+ { freq: 28.074, mode: 'FT8' }, { freq: 28.180, mode: 'FT4' },
+ { freq: 50.313, mode: 'FT8' }, { freq: 50.318, mode: 'FT4' },
+ { freq: 144.174, mode: 'FT8' },
+ ];
+ function inferDigitalMode(freqHz: number): string {
+ const mhz = freqHz / 1_000_000;
+ for (const d of DIGITAL_FREQS) {
+ if (Math.abs(mhz - d.freq) <= 0.003) return d.mode;
+ }
+ return '';
+ }
+
+ // User changed band/mode in the entry strip → push to the rig if CAT is up.
+ // Both calls are fire-and-forget; CAT will reflect back via cat:state.
+ function onBandUserChange(v: string) {
+ setBand(v);
+ noteManualEdit();
+ if (catState.enabled && catState.connected) {
+ const hz = qsyFreqHz(v, mode);
+ if (hz > 0) SetCATFrequency(hz).catch(() => {});
+ }
+ }
+ function onModeUserChange(v: string) {
+ setMode(v);
+ applyModePreset(v);
+ noteManualEdit();
+ if (catState.enabled && catState.connected) {
+ SetCATMode(v).catch(() => {});
+ }
+ }
+
+ const userEditedRef = useRef>(new Set());
+ const lastLookedUpRef = useRef('');
+ const rstUserEditedRef = useRef(false);
+
+ const [details, setDetails] = useState(emptyDetails);
+ const updateDetails = useCallback((patch: Partial) => {
+ setDetails((d) => ({ ...d, ...patch }));
+ }, []);
+ const prefix = useMemo(() => computePrefix(callsign), [callsign]);
+ // Bearing/distance from operator's home grid to the remote station —
+ // shown live in the entry strip (SP azimuth) and Info tab (LP + dist).
+
+ // === Logbook list ===
+ const [qsos, setQsos] = useState([]);
+ const [total, setTotal] = useState(0);
+ const [error, setError] = useState('');
+ const [saving, setSaving] = useState(false);
+ const [filterCallsign, setFilterCallsign] = useState('');
+ const [filterBand, setFilterBand] = useState('');
+ const [filterMode, setFilterMode] = useState('');
+ const [activeTab, setActiveTab] = useState('recent');
+
+ // === Modals ===
+ const [editingQSO, setEditingQSO] = useState(null);
+ const [deletingQSO, setDeletingQSO] = useState(null);
+ const [selectedId, setSelectedId] = useState(null);
+ const [showSettings, setShowSettings] = useState(false);
+ // Optional deep-link: which Preferences section to open. Cleared on
+ // close so the next plain "Preferences" launch reverts to default.
+ const [settingsSection, setSettingsSection] = useState(undefined);
+ const [showDeleteAll, setShowDeleteAll] = useState(false);
+ const [deletingAll, setDeletingAll] = useState(false);
+ const [ctyRefreshing, setCtyRefreshing] = useState(false);
+
+ // === ADIF ===
+ const [importing, setImporting] = useState(false);
+ const [importResult, setImportResult] = useState(null);
+ const [importErrorsOpen, setImportErrorsOpen] = useState(false);
+
+ // === Lookup + WB ===
+ const [lookupResult, setLookupResult] = useState(null);
+ const [lookupBusy, setLookupBusy] = useState(false);
+ const [lookupError, setLookupError] = useState('');
+ const lookupTimerRef = useRef(null);
+ const wbTimerRef = useRef(null);
+ const [wb, setWb] = useState(null);
+ const [wbBusy, setWbBusy] = useState(false);
+
+ // === Station ===
+ const [station, setStation] = useState({
+ callsign: '', operator: '',
+ my_grid: '', my_country: '', my_sota_ref: '', my_pota_ref: '',
+ });
+
+ // === Clock ===
+ const [utcNow, setUtcNow] = useState('');
+ useEffect(() => {
+ function tick() {
+ const d = new Date();
+ const p = (n: number) => String(n).padStart(2, '0');
+ setUtcNow(`${d.getUTCFullYear()}-${p(d.getUTCMonth()+1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}`);
+ }
+ tick();
+ const id = window.setInterval(tick, 1000);
+ return () => window.clearInterval(id);
+ }, []);
+
+ const refresh = useCallback(async () => {
+ try {
+ const list = await ListQSO({
+ callsign: filterCallsign, band: filterBand, mode: filterMode,
+ limit: 500, offset: 0,
+ } as any);
+ const n = await CountQSO();
+ setQsos(list);
+ setTotal(n);
+ setError('');
+ } catch (e: any) {
+ setError(String(e?.message ?? e));
+ }
+ }, [filterCallsign, filterBand, filterMode]);
+
+ const loadStation = useCallback(async () => {
+ try { setStation(await GetStationSettings()); } catch {}
+ }, []);
+ const loadCATCfg = useCallback(async () => {
+ try {
+ const c = await GetCATSettings();
+ if (c.digital_default) digitalDefaultRef.current = c.digital_default;
+ } catch {}
+ }, []);
+ const loadLists = useCallback(async () => {
+ try {
+ const l: ListsSettings = await GetListsSettings();
+ if (l.bands && l.bands.length) setBands(l.bands);
+ if (l.modes && l.modes.length) {
+ setModePresets(l.modes);
+ const names = l.modes.map((m) => m.name);
+ setModes(names);
+ setMode((cur) => names.includes(cur) ? cur : names[0]);
+ const preset = l.modes.find((m) => m.name === mode) ?? l.modes[0];
+ if (preset && !rstUserEditedRef.current) {
+ if (preset.default_rst_sent) setRstSent(preset.default_rst_sent);
+ if (preset.default_rst_rcvd) setRstRcvd(preset.default_rst_rcvd);
+ }
+ }
+ } catch {}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ function applyModePreset(m: string) {
+ if (rstUserEditedRef.current) return;
+ const p = modePresets.find((x) => x.name === m);
+ if (!p) return;
+ if (p.default_rst_sent) setRstSent(p.default_rst_sent);
+ if (p.default_rst_rcvd) setRstRcvd(p.default_rst_rcvd);
+ }
+
+ useEffect(() => { refresh(); }, [refresh]);
+ useEffect(() => {
+ (async () => {
+ try {
+ const st = await GetStartupStatus();
+ if (!st.ok) { setError(`Startup failed: ${st.err}\nDB path: ${st.db_path}`); return; }
+ } catch {}
+ loadStation();
+ loadLists();
+ loadCATCfg();
+ // Hydrate CAT state on mount (the backend may already be polling).
+ try { setCatState(await GetCATState()); } catch {}
+ })();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ // CAT live updates. Push freq/band/mode into the entry strip when the rig
+ // moves, unless the user just typed something (1.5s grace window).
+ useEffect(() => {
+ EventsOn('cat:state', (s: CATState) => {
+ setCatState(s);
+ if (!s?.connected) return;
+ if (Date.now() < catFreezeUntilRef.current) return;
+ const lk = locksRef.current;
+ if (!lk.freq && s.freq_hz && s.freq_hz > 0) {
+ setFreqMhz((s.freq_hz / 1_000_000).toFixed(5));
+ }
+ // RX freq (split only): backend follows ADIF — freq_hz = TX,
+ // freq_rx_hz = RX. Only set when the rig is in split, otherwise the
+ // field would duplicate TX for no reason. The freq lock covers both.
+ if (!lk.freq) {
+ if (s.split && s.freq_rx_hz && s.freq_rx_hz > 0) {
+ setRxFreqMhz((s.freq_rx_hz / 1_000_000).toFixed(5));
+ } else {
+ setRxFreqMhz('');
+ }
+ }
+ if (!lk.band && s.band) setBand(s.band);
+
+ // Mode resolution priority:
+ // 1. If freq matches a known digital watering hole, pick the specific
+ // mode for that hole (FT8 / FT4) — beats whatever CAT reports.
+ // 2. Else if CAT reports DATA (generic), use the user's configured
+ // default digital mode (FT8 by default).
+ // 3. Else trust CAT (SSB, CW, AM, FM…).
+ if (!lk.mode) {
+ const inferred = s.freq_hz ? inferDigitalMode(s.freq_hz) : '';
+ if (inferred) {
+ setMode(inferred);
+ } else if (s.mode === 'DATA') {
+ setMode(digitalDefaultRef.current || 'FT8');
+ } else if (s.mode) {
+ setMode(s.mode);
+ }
+ }
+ });
+ return () => { EventsOff('cat:state'); };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ async function save() {
+ if (!callsign.trim()) { setError('Callsign required'); return; }
+ setSaving(true); setError('');
+ try {
+ const freqHz = freqMhz.trim() ? Math.round(parseFloat(freqMhz) * 1_000_000) : undefined;
+ const rxFreqHz = rxFreqMhz.trim() ? Math.round(parseFloat(rxFreqMhz) * 1_000_000) : undefined;
+ const now = new Date();
+ const start = qsoStartedAt ?? now;
+ const end = (locks.end && qsoEndedAt) ? qsoEndedAt : now;
+ const payload: any = {
+ callsign: callsign.trim().toUpperCase(),
+ qso_date: start.toISOString(),
+ qso_date_off: end.toISOString(),
+ band, mode, freq_hz: freqHz, freq_rx_hz: rxFreqHz,
+ rst_sent: rstSent, rst_rcvd: rstRcvd,
+ grid: grid.trim().toUpperCase(),
+ name, qth, country, comment, notes: note,
+ state: details.state, cnty: details.cnty, address: details.address,
+ lat: details.lat, lon: details.lon,
+ dxcc: details.dxcc, cqz: details.cqz, ituz: details.ituz, cont: details.cont || undefined,
+ qsl_msg: details.qsl_msg, qsl_via: details.qsl_via,
+ ant_az: details.ant_az, ant_el: details.ant_el, ant_path: details.ant_path,
+ prop_mode: details.prop_mode,
+ my_rig: details.my_rig, my_antenna: details.my_antenna,
+ tx_pwr: details.tx_pwr,
+ sat_name: details.sat_name, sat_mode: details.sat_mode,
+ contest_id: details.contest_id,
+ srx: details.srx, stx: details.stx,
+ email: details.email,
+ };
+ await AddQSO(payload);
+ resetEntry();
+ await refresh();
+ } catch (e: any) {
+ setError(String(e?.message ?? e));
+ } finally { setSaving(false); }
+ }
+
+ // resetEntry clears the form for the next QSO. Triggered after a
+ // successful log AND by ESC. Locked values (band/mode/freq/start/end)
+ // are preserved so backdated batches stay productive.
+ function resetEntry() {
+ setCallsign(''); setComment(''); setNote('');
+ if (!locks.start) setQsoStartedAt(null);
+ if (!locks.end) setQsoEndedAt(null);
+ resetAutoFill();
+ setLookupError('');
+ rstUserEditedRef.current = false;
+ applyModePreset(mode);
+ setDetails((d) => ({
+ ...d,
+ state: '', cnty: '', address: '', lat: undefined, lon: undefined,
+ dxcc: undefined, cqz: undefined, ituz: undefined, cont: '',
+ qsl_msg: '', qsl_via: '',
+ contest_id: '', srx: undefined, stx: undefined,
+ email: '',
+ }));
+ }
+
+ function resetAutoFill() {
+ setName(''); setQth(''); setCountry(''); setGrid('');
+ setDetails((d) => ({
+ ...d,
+ state: '', cnty: '', address: '',
+ lat: undefined, lon: undefined,
+ dxcc: undefined, cqz: undefined, ituz: undefined, cont: '',
+ qsl_via: '',
+ email: '',
+ }));
+ userEditedRef.current.clear();
+ lastLookedUpRef.current = '';
+ setLookupResult(null);
+ }
+
+ async function openEdit(id: number) {
+ try { setEditingQSO(await GetQSO(id)); }
+ catch (e: any) { setError(String(e?.message ?? e)); }
+ }
+ async function onModalSave(q: QSO) {
+ try { await UpdateQSO(q as any); setEditingQSO(null); await refresh(); }
+ catch (err: any) { setError(String(err?.message ?? err)); }
+ }
+ function onModalDelete(id: number) {
+ const q = editingQSO; setEditingQSO(null);
+ if (q) setDeletingQSO(q); else askDelete(id);
+ }
+ function askDelete(id: number) {
+ const q = qsos.find((x) => x.id === id);
+ if (q) setDeletingQSO(q);
+ }
+ async function confirmDelete() {
+ if (!deletingQSO) return;
+ try {
+ await DeleteQSO(deletingQSO.id);
+ if (selectedId === deletingQSO.id) setSelectedId(null);
+ setDeletingQSO(null);
+ await refresh();
+ } catch (err: any) {
+ setError(String(err?.message ?? err));
+ setDeletingQSO(null);
+ }
+ }
+ async function confirmDeleteAll() {
+ if (deletingAll) return;
+ setDeletingAll(true);
+ try {
+ await DeleteAllQSO();
+ setSelectedId(null);
+ setShowDeleteAll(false);
+ await refresh();
+ } catch (err: any) { setError(String(err?.message ?? err)); }
+ finally { setDeletingAll(false); }
+ }
+
+ async function runWorkedBefore(call: string, dxccHint: number = 0) {
+ setWbBusy(true);
+ try { setWb(await WorkedBefore(call, dxccHint)); }
+ catch { setWb(null); }
+ finally { setWbBusy(false); }
+ }
+ async function runLookup(call: string) {
+ if (call !== lastLookedUpRef.current) resetAutoFill();
+ setLookupBusy(true);
+ try {
+ const r = await LookupCallsign(call);
+ setLookupResult(r);
+ lastLookedUpRef.current = call;
+ const ue = userEditedRef.current;
+ if (!ue.has('name')) setName(r.name ?? '');
+ if (!ue.has('qth')) setQth(r.qth ?? '');
+ if (!ue.has('country')) setCountry(r.country ?? '');
+ if (!ue.has('grid')) setGrid(r.grid ?? '');
+ setDetails((d) => ({
+ ...d,
+ address: d.address || (r.address ?? ''),
+ state: d.state || (r.state ?? ''),
+ cnty: d.cnty || (r.cnty ?? ''),
+ lat: d.lat ?? (r.lat || undefined),
+ lon: d.lon ?? (r.lon || undefined),
+ dxcc: d.dxcc ?? (r.dxcc || undefined),
+ cqz: d.cqz ?? (r.cqz || undefined),
+ ituz: d.ituz ?? (r.ituz || undefined),
+ cont: d.cont || (r.cont ?? ''),
+ email: d.email || (r.email ?? ''),
+ qsl_via: d.qsl_via || (r.qsl_via ?? ''),
+ }));
+ if (r.dxcc && r.dxcc > 0) runWorkedBefore(call, r.dxcc);
+ } catch (e: any) {
+ setLookupResult(null);
+ setLookupError(String(e?.message ?? e));
+ } finally { setLookupBusy(false); }
+ }
+ function scheduleLookup(value: string) {
+ setLookupError('');
+ if (lookupTimerRef.current) window.clearTimeout(lookupTimerRef.current);
+ if (wbTimerRef.current) window.clearTimeout(wbTimerRef.current);
+ const call = value.trim().toUpperCase();
+ if (call.length < 3) {
+ setLookupResult(null); setWb(null);
+ if (lastLookedUpRef.current !== '') resetAutoFill();
+ return;
+ }
+ lookupTimerRef.current = window.setTimeout(() => runLookup(call), 400);
+ wbTimerRef.current = window.setTimeout(() => runWorkedBefore(call), 150);
+ }
+ function onCallsignInput(v: string) {
+ const wasEmpty = callsign.trim() === '';
+ const isEmpty = v.trim() === '';
+ if (wasEmpty && !isEmpty && !locks.start) {
+ // First keystroke of a new QSO — freeze the start time so it doesn't
+ // drift even if the lookup or typing takes 30 seconds. Skip when
+ // start is locked: the user is back-entering a past QSO and set a
+ // specific time manually.
+ setQsoStartedAt(new Date());
+ } else if (isEmpty && !locks.start) {
+ // Callsign wiped → user abandoned this QSO; reset the timer.
+ setQsoStartedAt(null);
+ }
+ setCallsign(v);
+ scheduleLookup(v);
+ }
+ function markEdited(field: string) { userEditedRef.current.add(field); }
+
+ async function importAdif() {
+ if (importing) return;
+ setError('');
+ try {
+ const path = await OpenADIFFile();
+ if (!path) return;
+ setImporting(true);
+ setImportResult(null);
+ setImportErrorsOpen(false);
+ const res = await ImportADIF(path);
+ setImportResult(res);
+ await refresh();
+ } catch (e: any) { setError(String(e?.message ?? e)); }
+ finally { setImporting(false); }
+ }
+
+ const menus: Menu[] = useMemo(() => [
+ { name: 'file', label: 'File', items: [
+ { type: 'item', label: 'Import ADIF…', action: 'file.import', shortcut: 'Ctrl+O' },
+ { type: 'item', label: 'Export ADIF…', action: 'file.export', shortcut: 'Ctrl+E', disabled: true },
+ { type: 'separator' },
+ { type: 'item', label: 'Delete all QSOs…', action: 'file.deleteall', disabled: total === 0 },
+ { type: 'separator' },
+ { type: 'item', label: 'Exit', action: 'file.exit', shortcut: 'Ctrl+Q', disabled: true },
+ ]},
+ { name: 'edit', label: 'Edit', items: [
+ { type: 'item', label: 'Edit selected QSO…', action: 'edit.edit', shortcut: 'Enter', disabled: selectedId === null },
+ { type: 'item', label: 'Delete selected QSO', action: 'edit.delete', shortcut: 'Del', disabled: selectedId === null },
+ { type: 'separator' },
+ { type: 'item', label: 'Preferences…', action: 'edit.prefs' },
+ ]},
+ { name: 'view', label: 'View', items: [
+ { type: 'item', label: 'Refresh', action: 'view.refresh', shortcut: 'F5' },
+ { type: 'item', label: 'Clear filters', action: 'view.clearfilters' },
+ ]},
+ { name: 'tools', label: 'Tools', items: [
+ { type: 'item', label: 'Callsign lookup settings…', action: 'tools.lookup' },
+ { type: 'item', label: 'DX Cluster', action: 'tools.cluster', disabled: true },
+ { type: 'item', label: 'CAT control', action: 'tools.cat', disabled: true },
+ { type: 'item', label: 'Rotator', action: 'tools.rotator', disabled: true },
+ { type: 'separator' },
+ // Maintenance — bumped here while we only have one entry. Will move
+ // to a Tools → Maintenance submenu once Clublog + LoTW refresh land.
+ { type: 'item', label: ctyRefreshing ? 'Refreshing cty.dat…' : 'Refresh cty.dat', action: 'tools.refreshCty', disabled: ctyRefreshing },
+ ]},
+ { name: 'help', label: 'Help', items: [
+ { type: 'item', label: 'About HamLog', action: 'help.about', disabled: true },
+ ]},
+ ], [total, selectedId, ctyRefreshing]);
+
+ function handleMenu(action: string) {
+ switch (action) {
+ case 'file.import': importAdif(); break;
+ case 'file.deleteall': setShowDeleteAll(true); break;
+ case 'view.refresh': refresh(); break;
+ case 'view.clearfilters': setFilterCallsign(''); setFilterBand(''); setFilterMode(''); break;
+ case 'edit.edit': if (selectedId !== null) openEdit(selectedId); break;
+ case 'edit.delete': if (selectedId !== null) askDelete(selectedId); break;
+ case 'edit.prefs': setShowSettings(true); break;
+ case 'tools.lookup': setSettingsSection('lookup'); setShowSettings(true); break;
+ case 'tools.refreshCty': refreshCtyDat(); break;
+ }
+ }
+
+ async function refreshCtyDat() {
+ if (ctyRefreshing) return;
+ setCtyRefreshing(true);
+ setError('');
+ try {
+ const info = await RefreshCtyDat();
+ // Use the regular error banner area for a brief success note — keeps
+ // us from pulling in a toast system just for one maintenance action.
+ setError(`cty.dat refreshed — ${info.entities} entities loaded`);
+ setTimeout(() => setError((e) => e.startsWith('cty.dat refreshed') ? '' : e), 4000);
+ } catch (e: any) {
+ setError(`cty.dat refresh failed: ${String(e?.message ?? e)}`);
+ } finally {
+ setCtyRefreshing(false);
+ }
+ }
+
+ useEffect(() => {
+ function onKey(e: KeyboardEvent) {
+ const tag = (e.target as HTMLElement)?.tagName;
+ const typing = tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA';
+ if (e.key === 'F5') { e.preventDefault(); refresh(); return; }
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'o') {
+ e.preventDefault(); importAdif(); return;
+ }
+ if (typing) return;
+ if (selectedId !== null) {
+ if (e.key === 'Delete') { e.preventDefault(); askDelete(selectedId); return; }
+ if (e.key === 'Enter') { e.preventDefault(); openEdit(selectedId); return; }
+ }
+ }
+ window.addEventListener('keydown', onKey);
+ return () => window.removeEventListener('keydown', onKey);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [selectedId, refresh]);
+
+ return (
+
+ {/* ===== TOPBAR ===== */}
+ {compact ? (
+ // Minimal compact topbar — brand + freq + toggle. Saves vertical space
+ // so the single-row entry strip fits in a ~140px tall window.
+
+ ) : (
+
+
+
+
+
+
+
+ {freqMhz ? fmtFreqDots(freqMhz) : '—.———.———'}
+ {catState.split && rxFreqMhz && (
+
+ RX
+ {fmtFreqDots(rxFreqMhz)}
+
+ )}
+
+
MHz
+ {catState.split && (
+
SPLIT
+ )}
+
+
{band}
+
{mode}
+ {/* Bearing pill — clickable hook for the future rotator action.
+ Today it's a passive display; once the rotor backend lands we
+ wire onClick → rotate to short-path azimuth. */}
+ {(() => {
+ const p = pathBetween(station.my_grid, grid);
+ const disabled = !p;
+ return (
+
+ );
+ })()}
+ {catState.enabled && (
+ <>
+
+
+ {catState.connected
+ ? (catState.rig || 'CAT')
+ : (shortCatError(catState.error) || 'CAT off')}
+
+ {catState.backend === 'omnirig' && (
+
+ {[1, 2].map((n) => {
+ const active = (catState.rig_num || 1) === n;
+ return (
+
+ );
+ })}
+
+ )}
+ >
+ )}
+
+
+
+
+ {utcNow}UTC
+
+
+
+ {station.callsign ? (
+
+ ) : (
+
+ )}
+
+
{total.toLocaleString('en-US')}
+
Total QSOs
+
+
+
+
+ )}
+
+ {error && (
+
+
+
{error}
+
+
+ )}
+
+ {/* ===== ENTRY STRIP =====
+ Enter from any
inside the strip logs the QSO. Radix Selects
+ render as
+ );
+}
diff --git a/frontend/src/components/BandSlotGrid.tsx b/frontend/src/components/BandSlotGrid.tsx
new file mode 100644
index 0000000..fc59ff4
--- /dev/null
+++ b/frontend/src/components/BandSlotGrid.tsx
@@ -0,0 +1,167 @@
+import { useMemo } from 'react';
+import { Star, Radio } from 'lucide-react';
+import { Badge } from '@/components/ui/badge';
+import { cn } from '@/lib/utils';
+import type { WorkedBeforeView } from '@/types';
+
+type WorkedBefore = WorkedBeforeView;
+
+interface Props {
+ wb: WorkedBefore | null;
+ busy: boolean;
+ currentBand: string;
+ currentMode: string;
+}
+
+// 13-column band layout — no 4m, no SHF (per user preference).
+const BANDS: { tag: string; label: string }[] = [
+ { tag: '160m', label: '160' },
+ { tag: '80m', label: '80' },
+ { tag: '60m', label: '60' },
+ { tag: '40m', label: '40' },
+ { tag: '30m', label: '30' },
+ { tag: '20m', label: '20' },
+ { tag: '17m', label: '17' },
+ { tag: '15m', label: '15' },
+ { tag: '12m', label: '12' },
+ { tag: '10m', label: '10' },
+ { tag: '6m', label: '6' },
+ { tag: '2m', label: 'V' },
+ { tag: '70cm', label: 'U' },
+];
+const CLASSES = ['PH', 'CW', 'DIG'] as const;
+
+const PHONE_MODES = new Set(['SSB','USB','LSB','AM','FM','DIGITALVOICE','PHONE']);
+function classMatchesMode(cls: string, mode: string): boolean {
+ const u = (mode || '').toUpperCase();
+ if (cls === 'PH') return PHONE_MODES.has(u);
+ if (cls === 'CW') return u === 'CW';
+ return u !== '' && u !== 'CW' && !PHONE_MODES.has(u);
+}
+
+const STATUS_CLASSES: Record = {
+ call_c: 'bg-emerald-700 hover:bg-emerald-600',
+ call_w: 'bg-emerald-300 hover:bg-emerald-200',
+ dxcc_c: 'bg-indigo-800 hover:bg-indigo-700',
+ dxcc_w: 'bg-indigo-300 hover:bg-indigo-200',
+};
+
+function cellTitle(band: string, cls: string, status: string, current: boolean): string {
+ const desc =
+ status === 'call_c' ? 'This callsign confirmed' :
+ status === 'call_w' ? 'This callsign worked (not confirmed)' :
+ status === 'dxcc_c' ? 'Entity confirmed (other callsign)' :
+ status === 'dxcc_w' ? 'Entity worked (other callsign)' :
+ 'Never worked';
+ return `${band} ${cls}: ${desc}${current ? ' — current entry' : ''}`;
+}
+
+export function BandSlotGrid({ wb, busy, currentBand, currentMode }: Props) {
+ const dxcc = wb?.dxcc ?? 0;
+ const dxccName = wb?.dxcc_name ?? '';
+ const dxccCount = wb?.dxcc_count ?? 0;
+ const hasDxcc = dxcc > 0;
+ const newOne = hasDxcc && dxccCount === 0;
+
+ const statusMap = useMemo(() => {
+ const m = new Map();
+ for (const s of wb?.band_status ?? []) {
+ m.set(`${s.band}|${s.class}`, s.status);
+ }
+ return m;
+ }, [wb]);
+
+ return (
+
+
+ {newOne ? (
+ <>
+
+
+ NEW ONE
+
+
+ {dxccName || `DXCC #${dxcc}`}
+ {' '}· never worked this entity
+
+ >
+ ) : hasDxcc ? (
+ <>
+
+ {dxccName || `DXCC #${dxcc}`}
+
+
+ {dxccCount}{' '}
+ QSO{dxccCount > 1 ? 's' : ''} with this entity
+
+ >
+ ) : busy ? (
+
+
+ looking up…
+
+ ) : (
+
+ Type a callsign to see entity stats
+
+ )}
+
+
+
+
+
+ |
+ {BANDS.map((b) => (
+
+ {b.label}
+ |
+ ))}
+
+
+
+ {CLASSES.map((cls) => {
+ const classCurrent = classMatchesMode(cls, currentMode);
+ return (
+
+ |
+ {cls}
+ |
+ {BANDS.map((b) => {
+ const st = statusMap.get(`${b.tag}|${cls}`) ?? '';
+ const isCurrent = b.tag === currentBand && classCurrent;
+ return (
+ |
+ );
+ })}
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/frontend/src/components/CallHistoryPanel.tsx b/frontend/src/components/CallHistoryPanel.tsx
new file mode 100644
index 0000000..06e96ae
--- /dev/null
+++ b/frontend/src/components/CallHistoryPanel.tsx
@@ -0,0 +1,119 @@
+import { Star } from 'lucide-react';
+import { Badge } from '@/components/ui/badge';
+import type { WorkedBeforeView } from '@/types';
+
+type WorkedBefore = WorkedBeforeView;
+
+interface Props {
+ wb: WorkedBefore | null;
+ busy: boolean;
+ currentCall: string;
+}
+
+function fmtDate(s: any): string {
+ if (!s) return '';
+ const d = new Date(s);
+ if (isNaN(d.getTime())) return '';
+ const p = (n: number) => String(n).padStart(2, '0');
+ return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}`;
+}
+function fmtDateTime(s: any): string {
+ if (!s) return '';
+ const d = new Date(s);
+ if (isNaN(d.getTime())) return '';
+ const p = (n: number) => String(n).padStart(2, '0');
+ return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`;
+}
+
+export function CallHistoryPanel({ wb, busy, currentCall }: Props) {
+ const hasCall = currentCall.trim() !== '';
+ const count = wb?.count ?? 0;
+ const entries = wb?.entries ?? [];
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/ConfirmDialog.tsx b/frontend/src/components/ConfirmDialog.tsx
new file mode 100644
index 0000000..07fd552
--- /dev/null
+++ b/frontend/src/components/ConfirmDialog.tsx
@@ -0,0 +1,82 @@
+import { useState } from 'react';
+import {
+ AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogFooter,
+ AlertDialogTitle, AlertDialogDescription,
+ AlertDialogAction, AlertDialogCancel,
+} from '@/components/ui/alert-dialog';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { cn } from '@/lib/utils';
+import { buttonVariants } from '@/components/ui/button';
+
+interface Props {
+ title?: string;
+ message: string;
+ confirmLabel?: string;
+ cancelLabel?: string;
+ danger?: boolean;
+ /** When set, user must type this exact phrase before the confirm button enables. */
+ confirmPhrase?: string;
+ onConfirm: () => void;
+ onCancel: () => void;
+}
+
+export function ConfirmDialog({
+ title = 'Confirm',
+ message,
+ confirmLabel = 'Confirm',
+ cancelLabel = 'Cancel',
+ danger = false,
+ confirmPhrase = '',
+ onConfirm,
+ onCancel,
+}: Props) {
+ const [typed, setTyped] = useState('');
+ const enabled = confirmPhrase === '' || typed === confirmPhrase;
+
+ return (
+ { if (!o) onCancel(); }}>
+ { if (e.key === 'Enter' && enabled) onConfirm(); }}
+ >
+
+ {title}
+ {message}
+
+
+ {confirmPhrase && (
+
+
+ setTyped(e.target.value)}
+ className="font-mono"
+ />
+
+ )}
+
+
+ {cancelLabel}
+ { if (enabled) onConfirm(); }}
+ >
+ {confirmLabel}
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/DetailsPanel.tsx b/frontend/src/components/DetailsPanel.tsx
new file mode 100644
index 0000000..a2dbb88
--- /dev/null
+++ b/frontend/src/components/DetailsPanel.tsx
@@ -0,0 +1,256 @@
+import { useMemo, useState } from 'react';
+import { ChevronUp, Construction } from 'lucide-react';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Checkbox } from '@/components/ui/checkbox';
+import {
+ Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
+} from '@/components/ui/select';
+import { cn } from '@/lib/utils';
+import { pathBetween } from '@/lib/maidenhead';
+
+export interface DetailsState {
+ state: string;
+ cnty: string;
+ address: string;
+ lat?: number;
+ lon?: number;
+ // DXCC entity number + zones (filled from QRZ/HamQTH or cty.dat fallback).
+ // Editable so the operator can correct an obviously wrong auto-fill.
+ dxcc?: number;
+ cqz?: number;
+ ituz?: number;
+ cont: string;
+ qsl_msg: string;
+ qsl_via: string;
+ ant_az?: number;
+ ant_el?: number;
+ ant_path: string;
+ prop_mode: string;
+ my_rig: string;
+ my_antenna: string;
+ tx_pwr?: number;
+ sat_name: string;
+ sat_mode: string;
+ contest_id: string;
+ srx?: number;
+ stx?: number;
+ email: string;
+}
+
+interface Props {
+ callsign: string;
+ prefix: string;
+ operatorGrid: string; // station.my_grid — origin for bearing/distance
+ remoteGrid: string; // entry-strip Grid value — destination
+ details: DetailsState;
+ onChange: (patch: Partial) => void;
+}
+
+type TabName = 'info' | 'awards' | 'my' | 'extended';
+
+const PROP_MODES = ['NONE','AS','AUE','AUR','BS','ECH','EME','ES','F2','F2M','FAI','GWAVE','INTERNET','ION','IRL','LOS','MS','RPT','RS','SAT','TEP','TR'];
+
+function numOrUndef(v: string): number | undefined {
+ if (v === '') return undefined;
+ const n = parseFloat(v);
+ return isNaN(n) ? undefined : n;
+}
+
+// Compact field helper to keep the JSX dense.
+function Field({ label, span = 1, children }: { label: string; span?: 1 | 2 | 3 | 6; children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+ );
+}
+
+export function DetailsPanel({ callsign: _cs, prefix, operatorGrid, remoteGrid, details, onChange }: Props) {
+ const [open, setOpen] = useState(null);
+
+ // Bearing/distance from operator's home grid to the remote station.
+ // Recomputed only when either grid actually changes.
+ const path = useMemo(
+ () => pathBetween(operatorGrid, remoteGrid),
+ [operatorGrid, remoteGrid],
+ );
+ const fmtDeg = (n: number) => `${Math.round(n)}°`;
+ const fmtKm = (n: number) => `${Math.round(n).toLocaleString()} km`;
+
+ function toggle(t: TabName) { setOpen((prev) => (prev === t ? null : t)); }
+
+ const satelliteMode = !!details.sat_name || !!details.sat_mode || details.prop_mode === 'SAT';
+ function setSatellite(on: boolean) {
+ if (on) {
+ if (details.prop_mode !== 'SAT') onChange({ prop_mode: 'SAT' });
+ } else {
+ onChange({
+ sat_name: '', sat_mode: '',
+ ...(details.prop_mode === 'SAT' ? { prop_mode: '' } : {}),
+ });
+ }
+ }
+
+ const tabs: { key: TabName; label: string }[] = [
+ { key: 'info', label: 'Info (F2)' },
+ { key: 'awards', label: 'Awards (F3)' },
+ { key: 'my', label: 'My (F4)' },
+ { key: 'extended', label: 'Extended (F5)' },
+ ];
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/Menubar.tsx b/frontend/src/components/Menubar.tsx
new file mode 100644
index 0000000..8667306
--- /dev/null
+++ b/frontend/src/components/Menubar.tsx
@@ -0,0 +1,67 @@
+import { useState } from 'react';
+import {
+ DropdownMenu, DropdownMenuTrigger, DropdownMenuContent,
+ DropdownMenuItem, DropdownMenuSeparator, DropdownMenuShortcut,
+} from '@/components/ui/dropdown-menu';
+import { cn } from '@/lib/utils';
+
+export type MenuItem =
+ | { type: 'item'; label: string; action: string; shortcut?: string; disabled?: boolean }
+ | { type: 'separator' };
+
+export interface Menu {
+ name: string;
+ label: string;
+ items: MenuItem[];
+}
+
+interface Props {
+ menus: Menu[];
+ onAction: (action: string) => void;
+}
+
+export function Menubar({ menus, onAction }: Props) {
+ // Track which menu is open so hover-to-switch works like a desktop menubar.
+ const [openMenu, setOpenMenu] = useState(null);
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/QSOEditModal.tsx b/frontend/src/components/QSOEditModal.tsx
new file mode 100644
index 0000000..04507ae
--- /dev/null
+++ b/frontend/src/components/QSOEditModal.tsx
@@ -0,0 +1,373 @@
+import { useEffect, useMemo, useState } from 'react';
+import { Trash2 } from 'lucide-react';
+import {
+ Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Textarea } from '@/components/ui/textarea';
+import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
+import { Badge } from '@/components/ui/badge';
+import {
+ Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
+} from '@/components/ui/select';
+import { cn } from '@/lib/utils';
+import type { QSOForm } from '@/types';
+
+type QSO = QSOForm;
+
+const BANDS = ['160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','4m','2m','70cm','23cm'];
+const MODES = ['SSB','CW','FT8','FT4','RTTY','PSK31','AM','FM','DIGITALVOICE','MFSK','OLIVIA','JS8','JT65','JT9'];
+const QSL_STATUSES = [
+ { value: '_', label: '—' },
+ { value: 'Y', label: 'Yes' },
+ { value: 'N', label: 'No' },
+ { value: 'R', label: 'Requested' },
+ { value: 'I', label: 'Ignore' },
+];
+const PROP_MODES = ['_','AS','AUE','AUR','BS','ECH','EME','ES','F2','F2M','FAI','GWAVE','INTERNET','ION','IRL','LOS','MS','RPT','RS','SAT','TEP','TR'];
+
+interface Props {
+ qso: QSO;
+ onSave: (q: QSO) => void;
+ onDelete: (id: number) => void;
+ onClose: () => void;
+}
+
+function toLocalISO(d: any): string {
+ if (!d) return '';
+ const date = new Date(d);
+ if (isNaN(date.getTime())) return '';
+ const p = (n: number) => String(n).padStart(2, '0');
+ return `${date.getUTCFullYear()}-${p(date.getUTCMonth()+1)}-${p(date.getUTCDate())}T${p(date.getUTCHours())}:${p(date.getUTCMinutes())}`;
+}
+function parseLocalISO(s: string): string | null {
+ if (!s) return null;
+ const m = s.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/);
+ if (!m) return null;
+ return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:00.000Z`;
+}
+function stringifyExtras(e?: Record): string {
+ if (!e) return '';
+ return Object.entries(e).map(([k, v]) => `${k} = ${v}`).join('\n');
+}
+function parseExtras(t: string): Record | undefined {
+ const out: Record = {};
+ for (const raw of t.split('\n')) {
+ const line = raw.trim();
+ if (!line) continue;
+ const idx = line.indexOf('=');
+ if (idx < 0) continue;
+ const k = line.slice(0, idx).trim().toUpperCase();
+ const v = line.slice(idx + 1).trim();
+ if (k && v) out[k] = v;
+ }
+ return Object.keys(out).length ? out : undefined;
+}
+function numOrUndef(v: any): number | undefined {
+ if (v === '' || v === null || v === undefined) return undefined;
+ const n = typeof v === 'number' ? v : parseFloat(String(v));
+ return isNaN(n) ? undefined : n;
+}
+function intOrUndef(v: any): number | undefined {
+ const n = numOrUndef(v);
+ return n === undefined ? undefined : Math.trunc(n);
+}
+
+function F({ label, span = 1, children }: { label: string; span?: 1 | 2 | 3 | 6; children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+ );
+}
+
+function QslSelect({ value, onChange }: { value?: string; onChange: (v: string) => void }) {
+ return (
+
+ );
+}
+
+export function QSOEditModal({ qso, onSave, onDelete, onClose }: Props) {
+ const [draft, setDraft] = useState(() => JSON.parse(JSON.stringify(qso)));
+ const [freqMhz, setFreqMhz] = useState(draft.freq_hz ? String(draft.freq_hz / 1_000_000) : '');
+ const [freqRxMhz, setFreqRxMhz] = useState(draft.freq_rx_hz ? String(draft.freq_rx_hz / 1_000_000) : '');
+ const [dateOn, setDateOn] = useState(toLocalISO(draft.qso_date));
+ const [dateOff, setDateOff] = useState(toLocalISO(draft.qso_date_off));
+ const [extrasText, setExtrasText] = useState(stringifyExtras(draft.extras));
+ const [localErr, setLocalErr] = useState('');
+ const [saving, setSaving] = useState(false);
+
+ function set(key: K, value: QSO[K]) {
+ setDraft((d) => ({ ...d, [key]: value }));
+ }
+
+ function save() {
+ if (!draft.callsign?.trim()) { setLocalErr('Callsign required'); return; }
+ setSaving(true);
+ setLocalErr('');
+ const out: any = {
+ ...draft,
+ callsign: draft.callsign.trim().toUpperCase(),
+ grid: (draft.grid ?? '').trim().toUpperCase(),
+ gridsquare_ext: (draft.gridsquare_ext ?? '').trim().toUpperCase(),
+ station_callsign: (draft.station_callsign ?? '').trim().toUpperCase(),
+ operator: (draft.operator ?? '').trim().toUpperCase(),
+ my_grid: (draft.my_grid ?? '').trim().toUpperCase(),
+ my_gridsquare_ext: (draft.my_gridsquare_ext ?? '').trim().toUpperCase(),
+ iota: (draft.iota ?? '').trim().toUpperCase(),
+ sota_ref: (draft.sota_ref ?? '').trim().toUpperCase(),
+ pota_ref: (draft.pota_ref ?? '').trim().toUpperCase(),
+ my_iota: (draft.my_iota ?? '').trim().toUpperCase(),
+ my_sota_ref: (draft.my_sota_ref ?? '').trim().toUpperCase(),
+ my_pota_ref: (draft.my_pota_ref ?? '').trim().toUpperCase(),
+ qso_date: parseLocalISO(dateOn) ?? new Date().toISOString(),
+ qso_date_off: parseLocalISO(dateOff) ?? undefined,
+ freq_hz: freqMhz.trim() ? Math.round(parseFloat(freqMhz) * 1_000_000) : undefined,
+ freq_rx_hz: freqRxMhz.trim() ? Math.round(parseFloat(freqRxMhz) * 1_000_000) : undefined,
+ dxcc: intOrUndef(draft.dxcc),
+ cqz: intOrUndef(draft.cqz),
+ ituz: intOrUndef(draft.ituz),
+ age: intOrUndef(draft.age),
+ srx: intOrUndef(draft.srx),
+ stx: intOrUndef(draft.stx),
+ my_dxcc: intOrUndef(draft.my_dxcc),
+ my_cq_zone: intOrUndef(draft.my_cq_zone),
+ my_itu_zone: intOrUndef(draft.my_itu_zone),
+ lat: numOrUndef(draft.lat), lon: numOrUndef(draft.lon),
+ my_lat: numOrUndef(draft.my_lat), my_lon: numOrUndef(draft.my_lon),
+ ant_az: numOrUndef(draft.ant_az), ant_el: numOrUndef(draft.ant_el),
+ tx_pwr: numOrUndef(draft.tx_pwr),
+ extras: parseExtras(extrasText),
+ };
+ onSave(out);
+ }
+
+ useEffect(() => {
+ function onKey(e: KeyboardEvent) {
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) save();
+ }
+ window.addEventListener('keydown', onKey);
+ return () => window.removeEventListener('keydown', onKey);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ });
+
+ const extrasCount = useMemo(
+ () => (draft.extras ? Object.keys(draft.extras).length : 0),
+ [draft.extras],
+ );
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx
new file mode 100644
index 0000000..72621e1
--- /dev/null
+++ b/frontend/src/components/SettingsModal.tsx
@@ -0,0 +1,922 @@
+import { useEffect, useMemo, useState } from 'react';
+import {
+ ArrowDown, ArrowUp, Copy, Plus, Star, StarOff, Trash2,
+ ChevronDown, ChevronRight,
+ User, Database, Radio, Cog, Server, Award, Antenna as AntennaIcon,
+ Compass, Wifi, Construction,
+} from 'lucide-react';
+import {
+ GetLookupSettings, SaveLookupSettings, ClearLookupCache, TestLookupProvider,
+ GetListsSettings, SaveListsSettings,
+ GetCATSettings, SaveCATSettings,
+ ListProfiles, GetActiveProfile, SaveProfile, DeleteProfile, ActivateProfile, DuplicateProfile,
+} from '../../wailsjs/go/main/App';
+import type { profile as profileModels } from '../../wailsjs/go/models';
+import type { LookupSettingsForm, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
+import type { main as mainModels } from '../../wailsjs/go/models';
+
+import {
+ Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Textarea } from '@/components/ui/textarea';
+import { Checkbox } from '@/components/ui/checkbox';
+import {
+ Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
+} from '@/components/ui/select';
+import { cn } from '@/lib/utils';
+
+type LookupSettings = LookupSettingsForm;
+type StationSettings = StationSettingsForm;
+type ListsSettings = ListsSettingsForm;
+type ModePreset = ModePresetForm;
+type CATSettings = Omit;
+type Profile = Omit;
+
+const emptyProfile = (): Profile => ({
+ id: 0,
+ name: '',
+ callsign: '', operator: '',
+ my_grid: '', my_country: '',
+ my_state: '', my_cnty: '',
+ my_street: '', my_city: '', my_postal_code: '',
+ my_sota_ref: '', my_pota_ref: '',
+ my_rig: '', my_antenna: '',
+ tx_pwr: undefined,
+ is_active: false,
+ sort_order: 0,
+ created_at: '' as any,
+ updated_at: '' as any,
+});
+
+interface Props {
+ initialSection?: string;
+ onClose: () => void;
+ onSaved: () => void;
+}
+
+/* ====== Tree definition ======
+ Section IDs are stable strings — adding new ones means adding a panel below.
+ `disabled: true` greys them out and shows the "coming soon" placeholder. */
+type SectionId =
+ | 'station'
+ | 'profiles'
+ | 'lookup'
+ | 'lists-bands'
+ | 'lists-modes'
+ | 'cluster'
+ | 'backup'
+ | 'awards'
+ | 'cat'
+ | 'rotator'
+ | 'antenna'
+ | 'audio';
+
+type TreeNode =
+ | { kind: 'group'; label: string; icon?: any; defaultOpen?: boolean; children: TreeNode[] }
+ | { kind: 'item'; label: string; id: SectionId; disabled?: boolean };
+
+const TREE: TreeNode[] = [
+ {
+ kind: 'group', label: 'User Configuration', icon: User, defaultOpen: true, children: [
+ { kind: 'item', label: 'Station Information', id: 'station' },
+ { kind: 'item', label: 'Profiles (portable, home, contest)', id: 'profiles' },
+ ],
+ },
+ {
+ kind: 'group', label: 'Software Configuration', icon: Cog, defaultOpen: true, children: [
+ { kind: 'item', label: 'Callsign Lookup', id: 'lookup' },
+ { kind: 'group', label: 'Lists', icon: Database, defaultOpen: true, children: [
+ { kind: 'item', label: 'Bands', id: 'lists-bands' },
+ { kind: 'item', label: 'Modes & default RST', id: 'lists-modes' },
+ ]},
+ { kind: 'item', label: 'DX Cluster', id: 'cluster', disabled: true },
+ { kind: 'item', label: 'Backup / Export', id: 'backup', disabled: true },
+ { kind: 'item', label: 'Awards', id: 'awards', disabled: true },
+ ],
+ },
+ {
+ kind: 'group', label: 'Hardware Configuration', icon: Server, children: [
+ { kind: 'item', label: 'CAT interface (OmniRig)', id: 'cat' },
+ { kind: 'item', label: 'Rotator (PstRotator)', id: 'rotator', disabled: true },
+ { kind: 'item', label: 'Antenna (Ultrabeam)', id: 'antenna', disabled: true },
+ { kind: 'item', label: 'Audio devices', id: 'audio', disabled: true },
+ ],
+ },
+];
+
+// Map section id → friendly name (used in breadcrumb / placeholders).
+const SECTION_LABELS: Partial> = {
+ station: 'Station Information',
+ profiles: 'Profiles',
+ lookup: 'Callsign Lookup',
+ 'lists-bands': 'Bands',
+ 'lists-modes': 'Modes & default RST',
+ cluster: 'DX Cluster',
+ backup: 'Backup / Export',
+ awards: 'Awards',
+ cat: 'CAT interface',
+ rotator: 'Rotator',
+ antenna: 'Antenna',
+ audio: 'Audio devices',
+};
+
+// ===== Tree component =====
+
+interface TreeProps {
+ selected: SectionId;
+ onSelect: (id: SectionId) => void;
+}
+
+function Tree({ selected, onSelect }: TreeProps) {
+ return (
+
+ );
+}
+
+function TreeNodeView({
+ node, depth, selected, onSelect,
+}: { node: TreeNode; depth: number; selected: SectionId; onSelect: (id: SectionId) => void }) {
+ if (node.kind === 'item') {
+ const isActive = selected === node.id;
+ return (
+ { if (!node.disabled) onSelect(node.id); }}
+ disabled={node.disabled}
+ className={cn(
+ 'w-full text-left px-2 py-1.5 rounded-md text-[12.5px] transition-colors flex items-center',
+ isActive ? 'bg-accent text-accent-foreground font-semibold' : 'hover:bg-muted/60',
+ node.disabled && 'opacity-50 cursor-not-allowed italic',
+ )}
+ style={{ paddingLeft: 8 + depth * 14 }}
+ >
+ {node.label}
+ {node.disabled && (
+
+ )}
+
+ );
+ }
+ // group
+ const [open, setOpen] = useState(node.defaultOpen ?? false);
+ const Icon = node.icon;
+ return (
+
+
setOpen((v) => !v)}
+ className="w-full text-left px-2 py-1.5 rounded-md text-[12px] uppercase tracking-wider text-muted-foreground hover:text-foreground flex items-center gap-1.5 font-semibold"
+ style={{ paddingLeft: 8 + depth * 14 }}
+ >
+ {open ? : }
+ {Icon && }
+ {node.label}
+
+ {open && (
+
+ {node.children.map((c, i) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+// ===== Section content panels =====
+
+function SectionHeader({ title, hint }: { title: string; hint?: string }) {
+ return (
+
+ {title}
+ {hint && {hint}
}
+
+ );
+}
+
+function ComingSoon({ id, icon: Icon }: { id: SectionId; icon?: any }) {
+ const label = SECTION_LABELS[id] ?? id;
+ const IconCmp = Icon ?? Construction;
+ return (
+
+
+
{label}
+
Module coming soon.
+
+ );
+}
+
+export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
+ const [selected, setSelected] = useState((initialSection as SectionId) || 'station');
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [clearing, setClearing] = useState(false);
+ const [msg, setMsg] = useState('');
+ const [err, setErr] = useState('');
+
+ const [lookup, setLookup] = useState({
+ qrz_user: '', qrz_password: '',
+ hamqth_user: '', hamqth_password: '',
+ primary: '', failsafe: '',
+ cache_ttl_days: 30,
+ });
+ // Per-provider Test state — keeps the success/error feedback adjacent
+ // to the button. Cleared on the next test run for that provider.
+ type TestResult = { ok: boolean; msg: string };
+ const [lookupTest, setLookupTest] = useState>({});
+ const [lookupTesting, setLookupTesting] = useState>({});
+ // The Station Information panel now edits the full active profile
+ // (not a flat 6-field StationSettings). Profile selection happens in
+ // the Profiles panel; any edit here saves back to whichever profile
+ // is currently active.
+ const [activeProfile, setActiveProfile] = useState(null);
+ const updateActive = (patch: Partial) =>
+ setActiveProfile((p) => (p ? { ...p, ...patch } : p));
+ const [lists, setLists] = useState({ bands: [], modes: [] });
+ const [bandsText, setBandsText] = useState('');
+ const [catCfg, setCatCfg] = useState({
+ enabled: false, backend: 'omnirig', omnirig_rig: 1, poll_ms: 250, delay_ms: 0,
+ digital_default: 'FT8',
+ });
+ const [profiles, setProfiles] = useState([]);
+ // State for ProfilesPanel — lifted here because PANELS[selected]() calls
+ // the panel as a plain function, not as a JSX element, so any useState
+ // inside the panel function would violate the Rules of Hooks.
+ const [profileSelectedId, setProfileSelectedId] = useState(0);
+ const [profileNameDraft, setProfileNameDraft] = useState('');
+
+ async function reloadProfiles() {
+ try {
+ const list = await ListProfiles();
+ setProfiles(list);
+ // Refresh the active-profile editor in case activation changed.
+ const ap = await GetActiveProfile();
+ setActiveProfile(ap as Profile);
+ } catch (e: any) {
+ setErr(String(e?.message ?? e));
+ }
+ }
+
+ // Keep the ProfilesPanel selector in sync with the loaded list. If the
+ // currently-selected profile is gone (post-delete) or none is selected
+ // yet, default to the active one.
+ useEffect(() => {
+ if (!profiles.length) return;
+ const stillThere = profiles.some((p) => (p.id as number) === profileSelectedId);
+ if (!stillThere) {
+ const next = profiles.find((p) => p.is_active) ?? profiles[0];
+ setProfileSelectedId(next.id as number);
+ setProfileNameDraft(next.name);
+ }
+ }, [profiles, profileSelectedId]);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ const [l, ls, c, ap] = await Promise.all([
+ GetLookupSettings(), GetListsSettings(), GetCATSettings(), GetActiveProfile(),
+ ]);
+ setLookup(l);
+ setActiveProfile(ap as Profile);
+ setLists(ls);
+ await reloadProfiles();
+ setBandsText((ls.bands ?? []).join('\n'));
+ setCatCfg(c);
+ } catch (e: any) {
+ setErr(String(e?.message ?? e));
+ } finally {
+ setLoading(false);
+ }
+ })();
+ }, []);
+
+ function addMode() {
+ setLists((l) => ({
+ ...l,
+ modes: [...(l.modes ?? []), { name: '', default_rst_sent: '59', default_rst_rcvd: '59' }],
+ }));
+ }
+ function removeMode(i: number) {
+ setLists((l) => {
+ const next = [...(l.modes ?? [])];
+ next.splice(i, 1);
+ return { ...l, modes: next };
+ });
+ }
+ function moveMode(i: number, dir: -1 | 1) {
+ setLists((l) => {
+ const next = [...(l.modes ?? [])];
+ const j = i + dir;
+ if (j < 0 || j >= next.length) return l;
+ [next[i], next[j]] = [next[j], next[i]];
+ return { ...l, modes: next };
+ });
+ }
+ function updateMode(i: number, patch: Partial) {
+ setLists((l) => {
+ const next = [...(l.modes ?? [])];
+ next[i] = { ...next[i], ...patch } as ModePreset;
+ return { ...l, modes: next };
+ });
+ }
+
+ async function save() {
+ setSaving(true); setErr(''); setMsg('');
+ try {
+ // Bands: dedup, lowercase, trim.
+ const seen = new Set();
+ const bands: string[] = [];
+ for (const line of bandsText.split('\n')) {
+ const b = line.trim().toLowerCase();
+ if (b && !seen.has(b)) { seen.add(b); bands.push(b); }
+ }
+ const modes = (lists.modes ?? [])
+ .map((m) => ({
+ name: (m.name ?? '').trim().toUpperCase(),
+ default_rst_sent: (m.default_rst_sent ?? '').trim(),
+ default_rst_rcvd: (m.default_rst_rcvd ?? '').trim(),
+ }))
+ .filter((m) => m.name !== '');
+ await SaveListsSettings({ bands, modes } as any);
+
+ if (activeProfile) {
+ await SaveProfile({
+ ...activeProfile,
+ callsign: (activeProfile.callsign ?? '').trim().toUpperCase(),
+ operator: (activeProfile.operator ?? '').trim().toUpperCase(),
+ my_grid: (activeProfile.my_grid ?? '').trim().toUpperCase(),
+ my_sota_ref: (activeProfile.my_sota_ref ?? '').trim().toUpperCase(),
+ my_pota_ref: (activeProfile.my_pota_ref ?? '').trim().toUpperCase(),
+ } as any);
+ }
+ await SaveLookupSettings(lookup as any);
+ await SaveCATSettings(catCfg as any);
+
+ setMsg('Settings saved.');
+ onSaved();
+ setTimeout(onClose, 500);
+ } catch (e: any) {
+ setErr(String(e?.message ?? e));
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ async function clearCache() {
+ setClearing(true); setErr(''); setMsg('');
+ try {
+ await ClearLookupCache();
+ setMsg('Cache cleared.');
+ } catch (e: any) {
+ setErr(String(e?.message ?? e));
+ } finally {
+ setClearing(false);
+ }
+ }
+
+ async function testProvider(provider: 'qrz' | 'hamqth') {
+ setLookupTesting((s) => ({ ...s, [provider]: true }));
+ setLookupTest((s) => ({ ...s, [provider]: undefined }));
+ const user = provider === 'qrz' ? lookup.qrz_user : lookup.hamqth_user;
+ const pwd = provider === 'qrz' ? lookup.qrz_password : lookup.hamqth_password;
+ try {
+ const r = await TestLookupProvider(provider, '', user ?? '', pwd ?? '');
+ setLookupTest((s) => ({ ...s, [provider]: { ok: true, msg: `OK — ${r.callsign} (${r.name || r.country || 'no name'})` } }));
+ } catch (e: any) {
+ setLookupTest((s) => ({ ...s, [provider]: { ok: false, msg: String(e?.message ?? e) } }));
+ } finally {
+ setLookupTesting((s) => ({ ...s, [provider]: false }));
+ }
+ }
+
+ const breadcrumb = useMemo(() => SECTION_LABELS[selected] ?? selected, [selected]);
+
+ // === Section content renderers ===
+
+ function StationPanel() {
+ if (!activeProfile) {
+ return Loading profile…
;
+ }
+ const p = activeProfile;
+ return (
+ <>
+
+
+ >
+ );
+ }
+
+ // Profile actions — kept at the SettingsModal level so the ProfilesPanel
+ // renderer can stay hooks-free (the PANELS map calls it as a plain
+ // function, not as a JSX component).
+ const activeProfileObj = profiles.find((p) => p.is_active) ?? profiles[0];
+ const currentProfile = profiles.find((p) => (p.id as number) === profileSelectedId);
+
+ async function profileActivate() {
+ if (!currentProfile) return;
+ try { await ActivateProfile(currentProfile.id as number); await reloadProfiles(); onSaved(); }
+ catch (e: any) { setErr(String(e?.message ?? e)); }
+ }
+ async function profileRemove() {
+ if (!currentProfile) return;
+ if (!confirm(`Delete profile "${currentProfile.name}"? All its settings will be lost.`)) return;
+ try { await DeleteProfile(currentProfile.id as number); await reloadProfiles(); }
+ catch (e: any) { setErr(String(e?.message ?? e)); }
+ }
+ async function profileDuplicate() {
+ if (!currentProfile) return;
+ const name = prompt(`Name for the new profile (copy of "${currentProfile.name}"):`, `${currentProfile.name} Copy`);
+ if (!name?.trim()) return;
+ try {
+ const dup = await DuplicateProfile(currentProfile.id as number, name.trim());
+ await reloadProfiles();
+ setProfileSelectedId(dup.id as number);
+ setProfileNameDraft(dup.name);
+ } catch (e: any) { setErr(String(e?.message ?? e)); }
+ }
+ async function profileCreateBlank() {
+ const name = prompt('Name for the new profile:', 'New profile');
+ if (!name?.trim()) return;
+ try {
+ const blank = emptyProfile();
+ blank.name = name.trim();
+ const saved = await SaveProfile(blank as any);
+ await reloadProfiles();
+ setProfileSelectedId(saved.id as number);
+ setProfileNameDraft(saved.name);
+ } catch (e: any) { setErr(String(e?.message ?? e)); }
+ }
+ async function profileRenameCurrent() {
+ if (!currentProfile || profileNameDraft.trim() === currentProfile.name) return;
+ try {
+ await SaveProfile({ ...currentProfile, name: profileNameDraft.trim() } as any);
+ await reloadProfiles();
+ } catch (e: any) { setErr(String(e?.message ?? e)); }
+ }
+
+ function ProfilesPanel() {
+ const current = currentProfile;
+ const active = activeProfileObj;
+ return (
+ <>
+
+
+
+
+
+
+
+
+ setProfileNameDraft(e.target.value)}
+ onBlur={profileRenameCurrent}
+ placeholder="Profile name"
+ disabled={!current}
+ />
+ {current?.is_active && (
+
+ ACTIVE
+
+ )}
+
+
+
+
+
+ New
+
+
+ Duplicate
+
+
+ Set active
+
+
+ Delete
+
+
+
+ {current && !current.is_active && (
+
+ You're viewing {current.name}. The active profile is {active?.name} — its values are stamped on new QSOs. Click Set active to switch.
+
+ )}
+
+ >
+ );
+ }
+
+ function LookupPanel() {
+ // Per-row provider editor — kept inline because it's only used twice
+ // and needs closure access to the parent state.
+ const row = (
+ key: 'qrz' | 'hamqth', label: string, userField: 'qrz_user' | 'hamqth_user',
+ pwdField: 'qrz_password' | 'hamqth_password',
+ ) => {
+ const test = lookupTest[key];
+ const testing = lookupTesting[key];
+ const hasCreds = !!(lookup[userField] && lookup[pwdField]);
+ return (
+
+ | {label} |
+
+ setLookup((s) => ({ ...s, primary: key, failsafe: s.failsafe === key ? '' : s.failsafe }))}
+ disabled={!hasCreds}
+ />
+ |
+
+ setLookup((s) => ({ ...s, failsafe: key, primary: s.primary === key ? '' : s.primary }))}
+ disabled={!hasCreds || lookup.primary === key}
+ />
+ |
+
+ setLookup((s) => ({ ...s, [userField]: e.target.value }))}
+ placeholder="User"
+ autoComplete="off"
+ />
+ |
+
+ setLookup((s) => ({ ...s, [pwdField]: e.target.value }))}
+ placeholder="Password"
+ autoComplete="off"
+ />
+ |
+
+ testProvider(key)}
+ disabled={!hasCreds || testing}
+ title="Run a sample lookup against the active profile's callsign to verify credentials"
+ >
+ {testing ? 'Testing…' : 'Test'}
+
+ |
+
+ {test?.msg}
+ |
+
+ );
+ };
+ return (
+ <>
+
+
+
+
+
+ | Provider |
+ Primary |
+ Failsafe |
+ User |
+ Password |
+ |
+ Result |
+
+
+
+ {row('qrz', 'QRZ.com', 'qrz_user', 'qrz_password')}
+ {row('hamqth', 'HamQTH', 'hamqth_user', 'hamqth_password')}
+
+
+
+
+ Failsafe is consulted only when the Primary returns no match or errors. Set both to none (uncheck) during contests to skip the network entirely.
+
+
+
+
Cache
+
+ Successful lookups are cached locally so the same callsign isn't fetched twice. TTL controls how long before a fresh query is made.
+
+
+
+
+ setLookup((s) => ({ ...s, cache_ttl_days: parseInt(e.target.value) || 30 }))}
+ />
+
+
+ {clearing ? 'Clearing…' : 'Clear cache now'}
+
+
+
+ >
+ );
+ }
+
+ function BandsPanel() {
+ return (
+ <>
+
+