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. +
+
+
+ HamLog +
+
+ {freqMhz ? fmtFreqDots(freqMhz) : '—.———.———'} + MHz + {band} + {mode} +
+ {utcNow}Z +
+ {station.callsign && ( + {station.callsign} + )} + +
+ ) : ( +
+
+
+ HamLog + v0.1 +
+ + + +
+
+ {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 +
+ + + {/* In compact mode the entry strip is the whole app — hide everything + else and let the user re-expand with the topbar toggle. */} + {compact ? null : <> + {/* ===== BAND/SLOT GRID ===== */} + + + {/* ===== F2-F5 DETAILS ===== */} + + + {/* ===== LOWER: tabs+table | call history ===== */} +
+
+ + + Main + + Recent QSOs + {qsos.length} + + Cluster + Awards + Propagation + + + +
+ setFilterCallsign(e.target.value)} + /> + + + +
+ + {importResult && ( +
0 + ? 'bg-amber-50 border-amber-300 text-amber-800' + : 'bg-emerald-50 border-emerald-300 text-emerald-800', + )}> +
+ Import complete. + {importResult.imported} imported + {importResult.skipped} skipped + {importResult.total} total + {importResult.errors && importResult.errors.length > 0 && ( + + )} + +
+ {importErrorsOpen && importResult.errors && ( +
    + {importResult.errors.map((e, i) =>
  • {e}
  • )} +
+ )} +
+ )} + +
+ + + + {['Date UTC','Callsign','Band','Mode','MHz','RST sent','RST rcvd','Name','QTH','Country','Grid','Station','Comment',''].map((h, i) => ( + + ))} + + + + {qsos.length === 0 ? ( + + ) : qsos.map((q, i) => ( + setSelectedId(q.id)} + onDoubleClick={() => openEdit(q.id)} + > + + + + + + + + + + + + + + + + ))} + +
{h}
No QSO yet. Log your first contact above.
{fmtDateUTC(q.qso_date)}{q.callsign} + {q.band} + + {q.mode} + {fmtFreq(q.freq_hz)}{q.rst_sent ?? ''}{q.rst_rcvd ?? ''}{q.name ?? ''}{q.qth ?? ''}{q.country ?? ''}{q.grid ?? ''}{q.station_callsign ?? ''}{q.comment ?? ''} + + +
+
+
+ + {(['main','cluster','awards','propagation'] as const).map((t) => ( + + +
{t[0].toUpperCase() + t.slice(1)}
+
Module coming soon.
+
+ ))} +
+
+ + +
+ } + + {editingQSO && ( + setEditingQSO(null)} /> + )} + {showSettings && ( + { setShowSettings(false); setSettingsSection(undefined); }} + onSaved={() => { loadStation(); loadLists(); loadCATCfg(); }} + /> + )} + {deletingQSO && ( + setDeletingQSO(null)} + /> + )} + {showDeleteAll && ( + { if (!deletingAll) setShowDeleteAll(false); }} + /> + )} + + ); +} 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 + + )} +
+ + + + + + ))} + + + + {CLASSES.map((cls) => { + const classCurrent = classMatchesMode(cls, currentMode); + return ( + + + {BANDS.map((b) => { + const st = statusMap.get(`${b.tag}|${cls}`) ?? ''; + const isCurrent = b.tag === currentBand && classCurrent; + return ( + + ); + })} + +
+ {BANDS.map((b) => ( + + {b.label} +
+ {cls} + + ); + })} +
+
+ ); +} 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 ( +
+ + + {open === 'info' && ( +
+ + onChange({ state: e.target.value })} /> + + + onChange({ cnty: e.target.value })} /> + + + + + {/* DXCC #, CQ zone, ITU zone, Continent and Azimuth SP live in the + main entry strip — visible without opening F2. F2 keeps the + less-needed long-path bearing and both distances. */} + + + + + + + + + + + onChange({ address: e.target.value })} /> + + + onChange({ qsl_msg: e.target.value })} /> + + + onChange({ qsl_via: e.target.value })} /> + +
+ )} + + {open === 'awards' && ( +
+ +
Awards module coming soon
+
+ )} + + {open === 'my' && ( +
+ + onChange({ ant_az: numOrUndef(e.target.value) })} /> + + + onChange({ ant_el: numOrUndef(e.target.value) })} /> + + + onChange({ ant_path: e.target.value })} /> + + + + + + onChange({ tx_pwr: numOrUndef(e.target.value) })} /> + +
+ +
+ + onChange({ my_rig: e.target.value })} /> + + + onChange({ my_antenna: e.target.value })} /> + + {satelliteMode && ( + <> + + onChange({ sat_name: e.target.value })} /> + + + onChange({ sat_mode: e.target.value })} /> + + + )} +
+ )} + + {open === 'extended' && ( +
+ + onChange({ contest_id: e.target.value })} /> + + + onChange({ srx: numOrUndef(e.target.value) })} /> + + + onChange({ stx: numOrUndef(e.target.value) })} /> + + + onChange({ email: e.target.value })} /> + +
+ )} +
+ ); +} 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 ( + { if (!o) onClose(); }}> + + + Edit QSO + #{draft.id} — {draft.callsign} + Edit fields for QSO #{draft.id} + + + + + Basic + Contacted + QSL + Contest + Sat / Prop + My station + Notes + + Extras + {extrasCount > 0 && ( + {extrasCount} + )} + + + + {localErr && ( +
+ {localErr} +
+ )} + +
+ +
+ + set('callsign', e.target.value)} /> + + setDateOn(e.target.value)} /> + setDateOff(e.target.value)} /> + + + + + + + set('submode', e.target.value)} /> + + + + setFreqMhz(e.target.value)} /> + setFreqRxMhz(e.target.value)} /> + set('rst_sent', e.target.value)} /> + set('rst_rcvd', e.target.value)} /> + set('tx_pwr', numOrUndef(e.target.value) as any)} /> +
+
+ + +
+ set('name', e.target.value)} /> + set('qth', e.target.value)} /> + set('address', e.target.value)} /> + (set as any)('email', e.target.value)} /> + set('web', e.target.value)} /> + set('country', e.target.value)} /> + set('dxcc', intOrUndef(e.target.value) as any)} /> + set('cont', e.target.value)} /> + set('cqz', intOrUndef(e.target.value) as any)} /> + set('ituz', intOrUndef(e.target.value) as any)} /> + set('state', e.target.value)} /> + set('cnty', e.target.value)} /> + set('grid', e.target.value)} /> + set('gridsquare_ext', e.target.value)} /> + set('vucc_grids', e.target.value)} /> + set('iota', e.target.value)} /> + set('sota_ref', e.target.value)} /> + set('pota_ref', e.target.value)} /> + set('age', intOrUndef(e.target.value) as any)} /> + set('lat', numOrUndef(e.target.value) as any)} /> + set('lon', numOrUndef(e.target.value) as any)} /> + set('rig', e.target.value)} /> + set('ant', e.target.value)} /> +
+
+ + +
+ set('qsl_sent', v)} /> + set('qsl_rcvd', v)} /> + set('qsl_sent_date', e.target.value)} /> + set('qsl_rcvd_date', e.target.value)} /> + set('qsl_via', e.target.value)} /> + set('qsl_msg', e.target.value)} /> + set('qslmsg_rcvd', e.target.value)} /> + set('lotw_sent', v)} /> + set('lotw_rcvd', v)} /> + set('lotw_sent_date', e.target.value)} /> + set('lotw_rcvd_date', e.target.value)} /> + set('eqsl_sent', v)} /> + set('eqsl_rcvd', v)} /> + set('eqsl_sent_date', e.target.value)} /> + set('eqsl_rcvd_date', e.target.value)} /> + set('clublog_qso_upload_status', e.target.value)} /> + set('clublog_qso_upload_date', e.target.value)} /> + set('hrdlog_qso_upload_status', e.target.value)} /> + set('hrdlog_qso_upload_date', e.target.value)} /> +
+
+ + +
+ set('contest_id', e.target.value)} /> + set('srx', intOrUndef(e.target.value) as any)} /> + set('stx', intOrUndef(e.target.value) as any)} /> + set('srx_string', e.target.value)} /> + set('stx_string', e.target.value)} /> + set('check', e.target.value)} /> + set('precedence', e.target.value)} /> + set('arrl_sect', e.target.value)} /> +
+
+ + +
+ + + + set('sat_name', e.target.value)} /> + set('sat_mode', e.target.value)} /> + set('ant_az', numOrUndef(e.target.value) as any)} /> + set('ant_el', numOrUndef(e.target.value) as any)} /> + set('ant_path', e.target.value)} /> +
+
+ + +

These override the active station profile for this QSO only.

+
+ set('station_callsign', e.target.value)} /> + set('operator', e.target.value)} /> + set('my_grid', e.target.value)} /> + set('my_gridsquare_ext', e.target.value)} /> + set('my_country', e.target.value)} /> + set('my_state', e.target.value)} /> + set('my_cnty', e.target.value)} /> + set('my_dxcc', intOrUndef(e.target.value) as any)} /> + set('my_cq_zone', intOrUndef(e.target.value) as any)} /> + set('my_itu_zone', intOrUndef(e.target.value) as any)} /> + set('my_iota', e.target.value)} /> + set('my_sota_ref', e.target.value)} /> + set('my_pota_ref', e.target.value)} /> + set('my_lat', numOrUndef(e.target.value) as any)} /> + set('my_lon', numOrUndef(e.target.value) as any)} /> + set('my_street', e.target.value)} /> + set('my_city', e.target.value)} /> + set('my_postal_code', e.target.value)} /> + set('my_rig', e.target.value)} /> + set('my_antenna', e.target.value)} /> +
+
+ + +
+ set('comment', e.target.value)} /> +