From 8f8c17ed917a51cbd694ea86daee4b213a235908 Mon Sep 17 00:00:00 2001 From: Auc Date: Sat, 11 Jul 2026 15:47:24 +0800 Subject: [PATCH] Fix timer persistence and desktop reliability --- .gitignore | 1 + README.md | 13 +- native-macos/Package.swift | 4 + native-macos/README.md | 9 + .../Sources/StandForgeMac/StandForgeMac.swift | 225 ++- .../StandForgeTimerModelTests.swift | 55 + native-macos/scripts/build-app.sh | 15 + package-lock.json | 1210 ++++++++++------- package.json | 9 +- src-tauri/Cargo.lock | 677 +-------- src-tauri/Cargo.toml | 1 - src-tauri/capabilities/default.json | 3 +- src-tauri/src/commands.rs | 129 +- src-tauri/src/db.rs | 99 +- src-tauri/src/lib.rs | 26 +- src-tauri/src/models.rs | 10 + src-tauri/src/timer.rs | 220 ++- src-tauri/tauri.conf.json | 8 +- src/App.css | 116 -- src/App.tsx | 23 +- src/components/FloatingWindow.tsx | 135 +- src/components/ReminderWindow.tsx | 160 --- src/components/TimerDisplay.tsx | 459 ------- src/components/ui/card.tsx | 75 - src/index.css | 60 +- src/lib/types.ts | 2 +- src/stores/useConfigStore.test.ts | 43 + src/stores/useConfigStore.ts | 6 +- src/stores/useTimerStore.test.ts | 19 + src/stores/useTimerStore.ts | 22 +- vite.config.ts | 9 +- vitest.config.ts | 13 + 32 files changed, 1739 insertions(+), 2117 deletions(-) create mode 100644 native-macos/Tests/StandForgeMacTests/StandForgeTimerModelTests.swift delete mode 100644 src/App.css delete mode 100644 src/components/ReminderWindow.tsx delete mode 100644 src/components/TimerDisplay.tsx delete mode 100644 src/components/ui/card.tsx create mode 100644 src/stores/useConfigStore.test.ts create mode 100644 src/stores/useTimerStore.test.ts create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index 0f55ca1..e6464e8 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ dist-ssr .npm-cache/ .npm-logs/ native-macos/.build/ +native-macos/build/ src-tauri/standforge.db* # Editor directories and files diff --git a/README.md b/README.md index 1de9b30..f739be3 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,15 @@ StandForge is a small macOS/Tauri app that runs a background sitting timer and p - Background timer that keeps running after the settings window is closed - Always-on-top reminder popup when it is time to stand - Pause, resume, stop, snooze, and manual phase switching -- Local SQLite storage for cycle settings and completed stand sessions +- Local persistence for cycle settings, active timers, and completed stand sessions - Today view with completed sessions, standing time, snooze count, and completion rate ## Development This repository now has two app implementations: -- **Tauri/React**: the current cross-platform shell with CSS glass styling. -- **Native macOS**: a SwiftUI/AppKit floating window that uses system Liquid Glass on macOS 26+. +- **Tauri/React**: the current cross-platform shell with CSS glass styling and SQLite storage. +- **Native macOS**: a SwiftUI/AppKit floating window that uses system Liquid Glass on macOS 26+ and stores its state in `UserDefaults`. ```bash npm install @@ -50,3 +50,10 @@ npm run mac:native:build ``` The native app bundle is written to `native-macos/.build/app/StandForge Native.app`. + +Run the backend and native unit tests with: + +```bash +cargo test --locked --manifest-path src-tauri/Cargo.toml +swift test --package-path native-macos +``` diff --git a/native-macos/Package.swift b/native-macos/Package.swift index d9ffe32..ea49be8 100644 --- a/native-macos/Package.swift +++ b/native-macos/Package.swift @@ -14,6 +14,10 @@ let package = Package( .executableTarget( name: "StandForgeMac", path: "Sources/StandForgeMac" + ), + .testTarget( + name: "StandForgeMacTests", + dependencies: ["StandForgeMac"] ) ] ) diff --git a/native-macos/README.md b/native-macos/README.md index 280d932..9907e86 100644 --- a/native-macos/README.md +++ b/native-macos/README.md @@ -24,6 +24,15 @@ The app bundle is written to: native-macos/.build/app/StandForge Native.app ``` +Local builds use an ad-hoc signature. For a distributable Developer ID build, +provide a signing identity and an optional notarytool keychain profile: + +```bash +CODESIGN_IDENTITY="Developer ID Application: Example Corp (TEAMID)" \ +NOTARY_PROFILE="standforge-notary" \ +native-macos/scripts/build-app.sh +``` + ## Liquid Glass behavior The SwiftUI version uses `GlassEffectContainer`, `glassEffect(_:in:)`, and the diff --git a/native-macos/Sources/StandForgeMac/StandForgeMac.swift b/native-macos/Sources/StandForgeMac/StandForgeMac.swift index 4a84762..d8f0669 100644 --- a/native-macos/Sources/StandForgeMac/StandForgeMac.swift +++ b/native-macos/Sources/StandForgeMac/StandForgeMac.swift @@ -25,7 +25,9 @@ private final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotifica func applicationDidFinishLaunching(_ notification: Notification) { UNUserNotificationCenter.current().delegate = self - timerModel.requestNotificationPermission() + if timerModel.notificationsEnabled { + timerModel.requestNotificationPermission() + } createFloatingWindow() timerModel.startIfNeeded() } @@ -88,7 +90,7 @@ private final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotifica } } -private enum TimerPhase { +enum TimerPhase: String, Codable { case idle case sitting case standPending @@ -97,35 +99,88 @@ private enum TimerPhase { case paused } +struct NativeStandSession: Codable, Identifiable { + let id: UUID + let start: Date + let end: Date + let durationSeconds: Int + let snoozeCount: Int +} + +private struct PersistedTimerState: Codable { + let phase: TimerPhase + let deadline: Date? + let remainingSeconds: Int + let totalPhaseSeconds: Int + let phaseBeforePause: TimerPhase + let standStartedAt: Date? + let snoozeCount: Int +} + @MainActor -private final class StandForgeTimerModel: ObservableObject { +final class StandForgeTimerModel: ObservableObject { @Published var phase: TimerPhase = .idle @Published var remainingSeconds: Int = 45 * 60 @Published var totalPhaseSeconds: Int = 45 * 60 @Published var sitMinutes: Double { - didSet { defaults.set(sitMinutes, forKey: "sitMinutes") } + didSet { + defaults.set(sitMinutes, forKey: "sitMinutes") + if phase == .idle { + remainingSeconds = Int(sitMinutes * 60) + totalPhaseSeconds = remainingSeconds + } + } } @Published var standMinutes: Double { didSet { defaults.set(standMinutes, forKey: "standMinutes") } } @Published var notificationsEnabled: Bool { - didSet { defaults.set(notificationsEnabled, forKey: "notificationsEnabled") } + didSet { + defaults.set(notificationsEnabled, forKey: "notificationsEnabled") + if notificationsEnabled { + requestNotificationPermission() + } + } } @Published var soundEnabled: Bool { didSet { defaults.set(soundEnabled, forKey: "soundEnabled") } } + @Published private(set) var sessions: [NativeStandSession] - private let defaults = UserDefaults.standard + private let defaults: UserDefaults private var timer: Timer? private var phaseBeforePause: TimerPhase = .idle + private var deadline: Date? + private var standStartedAt: Date? + private var snoozeCount = 0 - init() { + init(defaults: UserDefaults = .standard) { + self.defaults = defaults sitMinutes = defaults.object(forKey: "sitMinutes") as? Double ?? 45 standMinutes = defaults.object(forKey: "standMinutes") as? Double ?? 15 notificationsEnabled = defaults.object(forKey: "notificationsEnabled") as? Bool ?? true soundEnabled = defaults.object(forKey: "soundEnabled") as? Bool ?? true - remainingSeconds = Int(sitMinutes * 60) - totalPhaseSeconds = Int(sitMinutes * 60) + sessions = Self.decodeSessions(from: defaults) + + if let data = defaults.data(forKey: "timerState"), + let persisted = try? JSONDecoder().decode(PersistedTimerState.self, from: data) { + phase = persisted.phase + deadline = persisted.deadline + totalPhaseSeconds = persisted.totalPhaseSeconds + phaseBeforePause = persisted.phaseBeforePause + standStartedAt = persisted.standStartedAt + snoozeCount = persisted.snoozeCount + if persisted.phase == .paused { + remainingSeconds = persisted.remainingSeconds + } else if let deadline = persisted.deadline { + remainingSeconds = max(0, Int(ceil(deadline.timeIntervalSinceNow))) + } else { + remainingSeconds = persisted.remainingSeconds + } + } else { + remainingSeconds = Int(sitMinutes * 60) + totalPhaseSeconds = Int(sitMinutes * 60) + } } var displaySeconds: Int { @@ -177,6 +232,22 @@ private final class StandForgeTimerModel: ObservableObject { } } + var todaySessions: [NativeStandSession] { + sessions + .filter { Calendar.current.isDateInToday($0.end) } + .sorted { $0.end > $1.end } + } + + var todayTotalDuration: Int { + todaySessions.reduce(0) { $0 + $1.durationSeconds } + } + + var todayCompletionRate: Int { + guard !todaySessions.isEmpty else { return 0 } + let target = max(1, Int(standMinutes * 60) * todaySessions.count) + return min(100, todayTotalDuration * 100 / target) + } + func primaryAction() { switch phase { case .idle: @@ -193,23 +264,37 @@ private final class StandForgeTimerModel: ObservableObject { } func startIfNeeded() { - guard phase == .idle else { return } - startSitting() + if phase == .idle { + startSitting() + return + } + if phase == .sitting || phase == .standing || phase == .snoozed { + scheduleTimer(keepDeadline: true) + tick() + } } func stop() { + completeStandingSessionIfNeeded() timer?.invalidate() timer = nil + deadline = nil phase = .idle totalPhaseSeconds = Int(sitMinutes * 60) remainingSeconds = totalPhaseSeconds + phaseBeforePause = .idle + snoozeCount = 0 + persistState() } func snooze(minutes: Int) { + guard phase == .standPending || phase == .snoozed else { return } phase = .snoozed totalPhaseSeconds = minutes * 60 remainingSeconds = totalPhaseSeconds + snoozeCount += 1 scheduleTimer() + persistState() } func startStandingNow() { @@ -221,34 +306,45 @@ private final class StandForgeTimerModel: ObservableObject { } private func startSitting() { + completeStandingSessionIfNeeded() phase = .sitting totalPhaseSeconds = Int(sitMinutes * 60) remainingSeconds = totalPhaseSeconds + snoozeCount = 0 scheduleTimer() + persistState() } private func startStanding() { phase = .standing totalPhaseSeconds = Int(standMinutes * 60) remainingSeconds = totalPhaseSeconds + standStartedAt = Date() scheduleTimer() + persistState() } private func pause() { guard phase != .idle, phase != .paused else { return } phaseBeforePause = phase phase = .paused + deadline = nil timer?.invalidate() timer = nil + persistState() } private func resume() { phase = phaseBeforePause == .idle ? .sitting : phaseBeforePause scheduleTimer() + persistState() } - private func scheduleTimer() { + private func scheduleTimer(keepDeadline: Bool = false) { timer?.invalidate() + if !keepDeadline || deadline == nil { + deadline = Date().addingTimeInterval(TimeInterval(remainingSeconds)) + } timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in Task { @MainActor in self?.tick() @@ -258,12 +354,14 @@ private final class StandForgeTimerModel: ObservableObject { private func tick() { guard phase == .sitting || phase == .standing || phase == .snoozed else { return } - remainingSeconds = max(0, remainingSeconds - 1) + guard let deadline else { return } + remainingSeconds = max(0, Int(ceil(deadline.timeIntervalSinceNow))) guard remainingSeconds == 0 else { return } timer?.invalidate() timer = nil + self.deadline = nil switch phase { case .sitting: @@ -277,6 +375,53 @@ private final class StandForgeTimerModel: ObservableObject { default: break } + persistState() + } + + private func completeStandingSessionIfNeeded() { + guard let standStartedAt else { return } + let end = Date() + let duration = max(0, Int(end.timeIntervalSince(standStartedAt))) + if duration > 0 { + sessions.append( + NativeStandSession( + id: UUID(), + start: standStartedAt, + end: end, + durationSeconds: duration, + snoozeCount: snoozeCount + ) + ) + sessions = Array(sessions.suffix(500)) + persistSessions() + } + self.standStartedAt = nil + } + + private func persistState() { + let state = PersistedTimerState( + phase: phase, + deadline: deadline, + remainingSeconds: remainingSeconds, + totalPhaseSeconds: totalPhaseSeconds, + phaseBeforePause: phaseBeforePause, + standStartedAt: standStartedAt, + snoozeCount: snoozeCount + ) + if let data = try? JSONEncoder().encode(state) { + defaults.set(data, forKey: "timerState") + } + } + + private func persistSessions() { + if let data = try? JSONEncoder().encode(sessions) { + defaults.set(data, forKey: "sessions") + } + } + + private static func decodeSessions(from defaults: UserDefaults) -> [NativeStandSession] { + guard let data = defaults.data(forKey: "sessions") else { return [] } + return (try? JSONDecoder().decode([NativeStandSession].self, from: data)) ?? [] } private func notify(title: String, subtitle: String, body: String) { @@ -499,6 +644,10 @@ private struct FloatingTimerWindow: View { sliderPanel(title: "屏幕使用", value: $model.sitMinutes, range: 5...90, step: 5) sliderPanel(title: "站立", value: $model.standMinutes, range: 3...30, step: 1) + + glassTextButton(title: "退出 StandForge", systemName: "power") { + NSApplication.shared.terminate(nil) + } } .padding(.bottom, 4) } @@ -507,20 +656,40 @@ private struct FloatingTimerWindow: View { private var todayContent: some View { VStack(spacing: 12) { HStack(spacing: 8) { - statPanel(title: "站立总时长", value: "0 分") - statPanel(title: "完成次数", value: "0") - statPanel(title: "完成率", value: "0%") + statPanel(title: "站立总时长", value: formatDuration(model.todayTotalDuration)) + statPanel(title: "完成次数", value: "\(model.todaySessions.count)") + statPanel(title: "完成率", value: "\(model.todayCompletionRate)%") } glassPanel { - VStack(spacing: 4) { - Text("今天还没有完成记录") - .font(.system(size: 14, weight: .semibold)) - Text("原生版本先提供 Liquid Glass 浮窗和提醒流程。") - .font(.system(size: 12)) - .foregroundStyle(.secondary) + if model.todaySessions.isEmpty { + VStack(spacing: 4) { + Text("今天还没有完成记录") + .font(.system(size: 14, weight: .semibold)) + Text("完成一次站立后会出现在这里。") + .font(.system(size: 12)) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity) + } else { + VStack(spacing: 0) { + ForEach(Array(model.todaySessions.prefix(4))) { session in + HStack { + VStack(alignment: .leading, spacing: 3) { + Text(session.start.formatted(date: .omitted, time: .shortened)) + .font(.system(size: 13, weight: .semibold).monospacedDigit()) + Text("延后 \(session.snoozeCount) 次") + .font(.system(size: 11)) + .foregroundStyle(.secondary) + } + Spacer() + Text(formatDuration(session.durationSeconds)) + .font(.system(size: 13, weight: .bold).monospacedDigit()) + } + .padding(.vertical, 8) + } + } } - .frame(maxWidth: .infinity) } } } @@ -579,7 +748,7 @@ private struct FloatingTimerWindow: View { Image(systemName: systemName) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(prominent ? .white : .primary) - .frame(width: 28, height: 28) + .frame(width: 36, height: 36) .contentShape(Circle()) .standForgeGlass(Circle(), interactive: true, tint: prominent ? .teal : nil) } @@ -616,6 +785,14 @@ private struct FloatingTimerWindow: View { let safeSeconds = max(0, seconds) return String(format: "%02d:%02d", safeSeconds / 60, safeSeconds % 60) } + + private func formatDuration(_ seconds: Int) -> String { + let minutes = seconds / 60 + if minutes > 0 { + return "\(minutes) 分" + } + return "\(seconds) 秒" + } } private struct StandForgeGlassContainer: View { diff --git a/native-macos/Tests/StandForgeMacTests/StandForgeTimerModelTests.swift b/native-macos/Tests/StandForgeMacTests/StandForgeTimerModelTests.swift new file mode 100644 index 0000000..bfda7e0 --- /dev/null +++ b/native-macos/Tests/StandForgeMacTests/StandForgeTimerModelTests.swift @@ -0,0 +1,55 @@ +import XCTest +@testable import StandForgeMac + +final class StandForgeTimerModelTests: XCTestCase { + private var defaults: UserDefaults! + private var suiteName: String! + + override func setUp() { + super.setUp() + suiteName = "StandForgeMacTests-\(UUID().uuidString)" + defaults = UserDefaults(suiteName: suiteName) + } + + override func tearDown() { + defaults.removePersistentDomain(forName: suiteName) + defaults = nil + suiteName = nil + super.tearDown() + } + + @MainActor + func testTimerStatePersistsAcrossModelRecreation() { + let model = StandForgeTimerModel(defaults: defaults) + model.primaryAction() + XCTAssertEqual(model.phase, .sitting) + + let restored = StandForgeTimerModel(defaults: defaults) + XCTAssertEqual(restored.phase, .sitting) + XCTAssertGreaterThan(restored.remainingSeconds, 0) + } + + @MainActor + func testSnoozeOnlyAppliesToPendingReminder() { + let model = StandForgeTimerModel(defaults: defaults) + model.snooze(minutes: 5) + XCTAssertEqual(model.phase, .idle) + + model.phase = .standPending + model.snooze(minutes: 5) + XCTAssertEqual(model.phase, .snoozed) + XCTAssertEqual(model.remainingSeconds, 300) + } + + @MainActor + func testStoppingStandingSessionCreatesHistory() async throws { + let model = StandForgeTimerModel(defaults: defaults) + model.phase = .standPending + model.startStandingNow() + try await Task.sleep(for: .milliseconds(1_100)) + model.stop() + + XCTAssertEqual(model.sessions.count, 1) + XCTAssertGreaterThanOrEqual(model.sessions[0].durationSeconds, 1) + } +} diff --git a/native-macos/scripts/build-app.sh b/native-macos/scripts/build-app.sh index 7346faa..e8712ce 100755 --- a/native-macos/scripts/build-app.sh +++ b/native-macos/scripts/build-app.sh @@ -3,6 +3,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +REPO_DIR="$(cd "$PROJECT_DIR/.." && pwd)" BUILD_DIR="$PROJECT_DIR/.build" APP_DIR="$BUILD_DIR/app/StandForge Native.app" EXECUTABLE="$BUILD_DIR/release/StandForgeMac" @@ -11,9 +12,11 @@ swift build --package-path "$PROJECT_DIR" -c release rm -rf "$APP_DIR" mkdir -p "$APP_DIR/Contents/MacOS" +mkdir -p "$APP_DIR/Contents/Resources" cp "$EXECUTABLE" "$APP_DIR/Contents/MacOS/StandForgeMac" chmod +x "$APP_DIR/Contents/MacOS/StandForgeMac" +cp "$REPO_DIR/src-tauri/icons/icon.icns" "$APP_DIR/Contents/Resources/AppIcon.icns" cat > "$APP_DIR/Contents/Info.plist" <<'PLIST' @@ -28,6 +31,8 @@ cat > "$APP_DIR/Contents/Info.plist" <<'PLIST' StandForge Native CFBundleDisplayName StandForge Native + CFBundleIconFile + AppIcon CFBundlePackageType APPL CFBundleShortVersionString @@ -42,4 +47,14 @@ cat > "$APP_DIR/Contents/Info.plist" <<'PLIST' PLIST +CODESIGN_IDENTITY="${CODESIGN_IDENTITY:--}" +codesign --force --deep --options runtime --sign "$CODESIGN_IDENTITY" "$APP_DIR" + +if [[ -n "${NOTARY_PROFILE:-}" ]]; then + ARCHIVE_PATH="$BUILD_DIR/StandForge-Native.zip" + ditto -c -k --keepParent "$APP_DIR" "$ARCHIVE_PATH" + xcrun notarytool submit "$ARCHIVE_PATH" --keychain-profile "$NOTARY_PROFILE" --wait + xcrun stapler staple "$APP_DIR" +fi + echo "$APP_DIR" diff --git a/package-lock.json b/package-lock.json index 4a0b80b..d500da6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,10 @@ "name": "standforge", "version": "0.1.0", "dependencies": { - "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@tauri-apps/api": "^2", - "@tauri-apps/plugin-opener": "^2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.563.0", @@ -32,7 +30,8 @@ "postcss": "^8.5.6", "tailwindcss": "^4.1.18", "typescript": "~5.8.3", - "vite": "^7.0.4" + "vite": "^7.0.4", + "vitest": "^3.2.4" } }, "node_modules/@alloc/quick-lru": { @@ -49,13 +48,13 @@ } }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -64,9 +63,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -74,21 +73,21 @@ } }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "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", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -105,14 +104,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", - "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -122,14 +121,14 @@ } }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -139,9 +138,9 @@ } }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -149,29 +148,29 @@ } }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -191,9 +190,9 @@ } }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -201,9 +200,9 @@ } }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -211,9 +210,9 @@ } }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -221,27 +220,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -283,33 +282,33 @@ } }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "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", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -317,14 +316,14 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -890,42 +889,6 @@ } } }, - "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-direction": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", @@ -941,73 +904,6 @@ } } }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -1026,30 +922,6 @@ } } }, - "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", @@ -1290,24 +1162,6 @@ } } }, - "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", @@ -1364,9 +1218,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", "cpu": [ "arm" ], @@ -1378,9 +1232,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", "cpu": [ "arm64" ], @@ -1392,9 +1246,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", "cpu": [ "arm64" ], @@ -1406,9 +1260,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", "cpu": [ "x64" ], @@ -1420,9 +1274,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", "cpu": [ "arm64" ], @@ -1434,9 +1288,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", "cpu": [ "x64" ], @@ -1448,9 +1302,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", "cpu": [ "arm" ], @@ -1462,9 +1316,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", "cpu": [ "arm" ], @@ -1476,9 +1330,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", "cpu": [ "arm64" ], @@ -1490,9 +1344,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", "cpu": [ "arm64" ], @@ -1504,9 +1358,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", "cpu": [ "loong64" ], @@ -1518,9 +1372,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", "cpu": [ "loong64" ], @@ -1532,9 +1386,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", "cpu": [ "ppc64" ], @@ -1546,9 +1400,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", "cpu": [ "ppc64" ], @@ -1560,9 +1414,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", "cpu": [ "riscv64" ], @@ -1574,9 +1428,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", "cpu": [ "riscv64" ], @@ -1588,9 +1442,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", "cpu": [ "s390x" ], @@ -1602,9 +1456,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", "cpu": [ "x64" ], @@ -1616,9 +1470,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", "cpu": [ "x64" ], @@ -1630,9 +1484,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", "cpu": [ "x64" ], @@ -1644,9 +1498,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", "cpu": [ "arm64" ], @@ -1658,9 +1512,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", "cpu": [ "arm64" ], @@ -1672,9 +1526,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", "cpu": [ "ia32" ], @@ -1686,9 +1540,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", "cpu": [ "x64" ], @@ -1700,9 +1554,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", "cpu": [ "x64" ], @@ -1936,6 +1790,66 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.7.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.7.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", @@ -2211,15 +2125,6 @@ "node": ">= 10" } }, - "node_modules/@tauri-apps/plugin-opener": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz", - "integrity": "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.8.0" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2265,10 +2170,28 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "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==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -2313,16 +2236,129 @@ "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==", + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, "license": "MIT", "dependencies": { - "tslib": "^2.0.0" + "tinyspy": "^4.0.3" }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/autoprefixer": { @@ -2363,13 +2399,16 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/browserslist": { @@ -2406,10 +2445,20 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/caniuse-lite": { - "version": "1.0.30001767", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", - "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", + "version": "1.0.30001803", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", "dev": true, "funding": [ { @@ -2427,6 +2476,33 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -2480,6 +2556,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2490,12 +2576,6 @@ "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.283", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", @@ -2517,6 +2597,13 @@ "node": ">=10.13.0" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -2569,6 +2656,26 @@ "node": ">=6" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2626,15 +2733,6 @@ "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", @@ -2946,6 +3044,13 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2983,9 +3088,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -3008,6 +3113,23 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3016,9 +3138,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -3029,9 +3151,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -3049,7 +3171,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3095,83 +3217,14 @@ "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.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -3181,31 +3234,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" } }, @@ -3225,6 +3278,13 @@ "semver": "bin/semver.js" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3235,6 +3295,40 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", @@ -3266,6 +3360,20 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3283,11 +3391,35 @@ "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/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } }, "node_modules/typescript": { "version": "5.8.3", @@ -3334,57 +3466,14 @@ "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": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -3452,6 +3541,119 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index a462b3c..71a4ff0 100644 --- a/package.json +++ b/package.json @@ -6,18 +6,20 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", + "test": "npm run test:frontend && npm run test:rust && npm run test:swift", + "test:frontend": "vitest run", + "test:rust": "cargo test --locked --manifest-path src-tauri/Cargo.toml", + "test:swift": "swift test --package-path native-macos", "preview": "vite preview", "tauri": "tauri", "mac:native:run": "swift run --package-path native-macos StandForgeMac", "mac:native:build": "native-macos/scripts/build-app.sh" }, "dependencies": { - "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@tauri-apps/api": "^2", - "@tauri-apps/plugin-opener": "^2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.563.0", @@ -36,6 +38,7 @@ "postcss": "^8.5.6", "tailwindcss": "^4.1.18", "typescript": "~5.8.3", - "vite": "^7.0.4" + "vite": "^7.0.4", + "vitest": "^3.2.4" } } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7b8c479..5925618 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -65,137 +65,6 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "async-signal" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "atk" version = "0.18.2" @@ -276,19 +145,6 @@ dependencies = [ "objc2", ] -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - [[package]] name = "brotli" version = "8.0.2" @@ -312,9 +168,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" @@ -471,15 +327,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "convert_case" version = "0.4.0" @@ -811,33 +658,6 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" -[[package]] -name = "endi" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" - -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -855,37 +675,6 @@ dependencies = [ "typeid", ] -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - [[package]] name = "fallible-iterator" version = "0.3.0" @@ -898,12 +687,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - [[package]] name = "fdeflate" version = "0.3.7" @@ -993,24 +776,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1019,28 +802,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-lite" -version = "2.6.1" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -1049,21 +819,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-io", @@ -1072,7 +842,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1419,12 +1188,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - [[package]] name = "hex" version = "0.4.3" @@ -1484,9 +1247,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.8.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1497,7 +1260,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1505,14 +1267,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1703,38 +1464,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is-docker" -version = "0.2.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" -dependencies = [ - "once_cell", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", -] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "itoa" @@ -1789,11 +1521,12 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -1890,11 +1623,10 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ - "bitflags 2.10.0", "libc", ] @@ -1909,12 +1641,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - [[package]] name = "litemap" version = "0.8.1" @@ -2095,9 +1821,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -2105,9 +1831,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", @@ -2333,34 +2059,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "open" -version = "5.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" -dependencies = [ - "dunce", - "is-wsl", - "libc", - "pathdiff", -] - [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - [[package]] name = "pango" version = "0.18.3" @@ -2386,12 +2090,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - [[package]] name = "parking_lot" version = "0.12.5" @@ -2415,12 +2113,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - [[package]] name = "percent-encoding" version = "2.3.2" @@ -2567,23 +2259,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - [[package]] name = "pkg-config" version = "0.3.32" @@ -2616,20 +2291,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix", - "windows-sys 0.61.2", -] - [[package]] name = "potential_utf" version = "0.1.4" @@ -2966,19 +2627,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -2987,9 +2635,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -3272,16 +2920,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - [[package]] name = "simd-adler32" version = "0.3.8" @@ -3376,6 +3014,19 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "standforge" +version = "0.1.0" +dependencies = [ + "chrono", + "rusqlite", + "serde", + "serde_json", + "tauri", + "tauri-build", + "uuid", +] + [[package]] name = "string_cache" version = "0.8.9" @@ -3581,20 +3232,6 @@ dependencies = [ "windows", ] -[[package]] -name = "standforge" -version = "0.1.0" -dependencies = [ - "chrono", - "rusqlite", - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-opener", - "uuid", -] - [[package]] name = "tauri-build" version = "2.5.3" @@ -3658,45 +3295,6 @@ dependencies = [ "tauri-utils", ] -[[package]] -name = "tauri-plugin" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1d0a4860b7ff570c891e1d2a586bf1ede205ff858fbc305e0b5ae5d14c1377" -dependencies = [ - "anyhow", - "glob", - "plist", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri-utils", - "toml 0.9.11+spec-1.1.0", - "walkdir", -] - -[[package]] -name = "tauri-plugin-opener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc624469b06f59f5a29f874bbc61a2ed737c0f9c23ef09855a292c389c42e83f" -dependencies = [ - "dunce", - "glob", - "objc2-app-kit", - "objc2-foundation", - "open", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", - "url", - "windows", - "zbus", -] - [[package]] name = "tauri-runtime" version = "2.9.2" @@ -3798,19 +3396,6 @@ dependencies = [ "toml 0.9.11+spec-1.1.0", ] -[[package]] -name = "tempfile" -version = "3.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - [[package]] name = "tendril" version = "0.4.3" @@ -4043,20 +3628,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags 2.10.0", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -4078,21 +3663,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "tracing-core" version = "0.1.36" @@ -4142,17 +3715,6 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" -[[package]] -name = "uds_windows" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" -dependencies = [ - "memoffset", - "tempfile", - "winapi", -] - [[package]] name = "unic-char-property" version = "0.9.0" @@ -4326,18 +3888,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -4348,23 +3910,21 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4372,9 +3932,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -4385,9 +3945,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -4407,9 +3967,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", @@ -4950,9 +4510,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.51.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" @@ -5049,67 +4609,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zbus" -version = "5.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" -dependencies = [ - "async-broadcast", - "async-executor", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", - "futures-core", - "futures-lite", - "hex", - "libc", - "ordered-stream", - "rustix", - "serde", - "serde_repr", - "tracing", - "uds_windows", - "uuid", - "windows-sys 0.61.2", - "winnow 0.7.14", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "5.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.114", - "zbus_names", - "zvariant", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" -dependencies = [ - "serde", - "winnow 0.7.14", - "zvariant", -] - [[package]] name = "zerocopy" version = "0.8.37" @@ -5189,43 +4688,3 @@ name = "zmij" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" - -[[package]] -name = "zvariant" -version = "5.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" -dependencies = [ - "endi", - "enumflags2", - "serde", - "winnow 0.7.14", - "zvariant_derive", - "zvariant_utils", -] - -[[package]] -name = "zvariant_derive" -version = "5.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" -dependencies = [ - "proc-macro-crate 3.4.0", - "proc-macro2", - "quote", - "syn 2.0.114", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 2.0.114", - "winnow 0.7.14", -] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fa21dfa..11d3b1f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -19,7 +19,6 @@ tauri-build = { version = "2", features = [] } [dependencies] tauri = { version = "2", features = ["macos-private-api"] } -tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" rusqlite = { version = "0.30", features = ["bundled"] } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index f3f305e..258630a 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -6,7 +6,6 @@ "permissions": [ "core:default", "core:window:allow-start-dragging", - "core:window:allow-set-size", - "opener:default" + "core:window:allow-set-size" ] } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 090c5d5..f2a200e 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,8 +1,10 @@ -use tauri::{Manager, State}; use std::sync::{Arc, Mutex}; -use crate::timer::StandTimer; + +use tauri::{Manager, State}; + use crate::db; use crate::models::{CycleConfig, StandSession}; +use crate::timer::{StandTimer, TimerState}; #[derive(Debug)] pub struct AppState { @@ -19,10 +21,14 @@ pub struct TodayStatsResponse { pub target_stand_sec: i32, } +fn persist_timer(timer: &StandTimer) -> Result<(), String> { + db::save_timer_state(&timer.snapshot()).map_err(|error| error.to_string()) +} + // Get current timer state #[tauri::command] pub fn get_timer_state(state: State) -> Result { - let timer = state.timer.lock().unwrap(); + let timer = state.timer.lock().map_err(|error| error.to_string())?; Ok(TimerStateResponse { status: timer.get_state().as_str().to_string(), current_phase: timer.get_current_phase().as_str().to_string(), @@ -48,11 +54,14 @@ pub fn start_timer( user_id: String, device_id: String, ) -> Result { + let timer = state.timer.lock().map_err(|error| error.to_string())?; + if timer.get_state() != TimerState::Idle { + return Err("计时器已经在运行".to_string()); + } let session_id = db::create_session(&user_id, &device_id) .map_err(|e| e.to_string())?; - - let timer = state.timer.lock().unwrap(); timer.start_sitting(session_id.clone()); + persist_timer(&timer)?; Ok(session_id) } @@ -61,7 +70,13 @@ pub fn start_timer( #[tauri::command] pub fn confirm_stand(state: State) -> Result<(), String> { let session_id = { - let timer = state.timer.lock().unwrap(); + let timer = state.timer.lock().map_err(|error| error.to_string())?; + if !matches!( + timer.get_state(), + TimerState::StandPending | TimerState::Snoozed + ) { + return Err("当前状态不能确认站立".to_string()); + } timer.get_current_session_id() }; if let Some(session_id) = session_id { @@ -69,18 +84,19 @@ pub fn confirm_stand(state: State) -> Result<(), String> { .map_err(|e| e.to_string())?; } - let timer = state.timer.lock().unwrap(); + let timer = state.timer.lock().map_err(|error| error.to_string())?; timer.confirm_stand(); - Ok(()) + persist_timer(&timer) } // User confirms they have sat down #[tauri::command] -pub fn confirm_sit( - state: State, -) -> Result { +pub fn confirm_sit(state: State) -> Result { let session_id = { - let timer = state.timer.lock().unwrap(); + let timer = state.timer.lock().map_err(|error| error.to_string())?; + if timer.get_state() != TimerState::Standing { + return Err("当前状态不能确认坐下".to_string()); + } timer.get_current_session_id() }; @@ -94,40 +110,57 @@ pub fn confirm_sit( let next_session_id = db::create_session("default_user", "this_mac") .map_err(|e| e.to_string())?; - let timer = state.timer.lock().unwrap(); + let timer = state.timer.lock().map_err(|error| error.to_string())?; timer.start_next_sitting(next_session_id); + persist_timer(&timer)?; Ok(duration) } // Pause the timer #[tauri::command] pub fn pause_timer(state: State) -> Result<(), String> { - let timer = state.timer.lock().unwrap(); + let timer = state.timer.lock().map_err(|error| error.to_string())?; + if !matches!(timer.get_state(), TimerState::Sitting | TimerState::Standing) { + return Err("当前状态不能暂停".to_string()); + } timer.pause(); - Ok(()) + persist_timer(&timer) } // Resume the timer #[tauri::command] pub fn resume_timer(state: State) -> Result<(), String> { - let timer = state.timer.lock().unwrap(); + let timer = state.timer.lock().map_err(|error| error.to_string())?; + if timer.get_state() != TimerState::Paused { + return Err("计时器没有暂停".to_string()); + } timer.resume(); - Ok(()) + persist_timer(&timer) } // Stop and reset the timer #[tauri::command] pub fn stop_timer(state: State) -> Result<(), String> { - let timer = state.timer.lock().unwrap(); + let timer = state.timer.lock().map_err(|error| error.to_string())?; + if let Some(session_id) = timer.get_current_session_id() { + db::finish_or_discard_session(&session_id, "user_stop") + .map_err(|error| error.to_string())?; + } timer.stop(); - Ok(()) + db::clear_timer_state().map_err(|error| error.to_string()) } // Switch to standing phase #[tauri::command] pub fn switch_to_stand(state: State) -> Result<(), String> { let session_id = { - let timer = state.timer.lock().unwrap(); + let timer = state.timer.lock().map_err(|error| error.to_string())?; + if !matches!( + timer.get_state(), + TimerState::Sitting | TimerState::StandPending | TimerState::Snoozed + ) { + return Err("当前状态不能切换到站立".to_string()); + } timer.get_current_session_id() }; if let Some(session_id) = session_id { @@ -135,24 +168,46 @@ pub fn switch_to_stand(state: State) -> Result<(), String> { .map_err(|e| e.to_string())?; } - let timer = state.timer.lock().unwrap(); + let timer = state.timer.lock().map_err(|error| error.to_string())?; timer.switch_to_stand(); - Ok(()) + persist_timer(&timer) } // Switch to sitting phase #[tauri::command] pub fn switch_to_sit(state: State) -> Result<(), String> { - let timer = state.timer.lock().unwrap(); - timer.switch_to_sit(); - Ok(()) + let session_id = { + let timer = state.timer.lock().map_err(|error| error.to_string())?; + if timer.get_state() != TimerState::Standing { + return Err("当前状态不能切换到坐姿".to_string()); + } + timer.get_current_session_id() + }; + if let Some(session_id) = session_id { + db::update_session_end(&session_id, "manual_switch") + .map_err(|error| error.to_string())?; + } + let next_session_id = db::create_session("default_user", "this_mac") + .map_err(|error| error.to_string())?; + let timer = state.timer.lock().map_err(|error| error.to_string())?; + timer.start_next_sitting(next_session_id); + persist_timer(&timer) } // Snooze the standing reminder #[tauri::command] pub fn snooze_stand(state: State, snooze_minutes: i32) -> Result<(), String> { + if !(1..=120).contains(&snooze_minutes) { + return Err("延后时间必须在 1 到 120 分钟之间".to_string()); + } let session_id = { - let timer = state.timer.lock().unwrap(); + let timer = state.timer.lock().map_err(|error| error.to_string())?; + if !matches!( + timer.get_state(), + TimerState::StandPending | TimerState::Snoozed + ) { + return Err("当前状态不能延后站立提醒".to_string()); + } timer.get_current_session_id() }; if let Some(session_id) = session_id { @@ -160,9 +215,9 @@ pub fn snooze_stand(state: State, snooze_minutes: i32) -> Result<(), S .map_err(|e| e.to_string())?; } - let timer = state.timer.lock().unwrap(); + let timer = state.timer.lock().map_err(|error| error.to_string())?; timer.snooze(snooze_minutes as i64); - Ok(()) + persist_timer(&timer) } #[tauri::command] @@ -175,10 +230,26 @@ pub fn update_config_command( state: State, config: CycleConfig, ) -> Result<(), String> { + if !(5..=180).contains(&config.sit_minutes) { + return Err("屏幕使用时长必须在 5 到 180 分钟之间".to_string()); + } + if !(1..=120).contains(&config.stand_minutes) { + return Err("站立时长必须在 1 到 120 分钟之间".to_string()); + } + if !(60..=86_400).contains(&config.auto_end_after_sec) { + return Err("自动结束时长必须在 60 秒到 24 小时之间".to_string()); + } + if !matches!(config.ui_skin.as_str(), "classic" | "liquid_glass") { + return Err("未知的界面皮肤".to_string()); + } db::update_config(&config).map_err(|e| e.to_string())?; - let timer = state.timer.lock().unwrap(); + let timer = state.timer.lock().map_err(|error| error.to_string())?; timer.set_durations(config.sit_minutes as i64, config.stand_minutes as i64); + timer.set_auto_end( + config.auto_end_enabled, + config.auto_end_after_sec as i64, + ); Ok(()) } diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 0f48625..4080763 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -1,7 +1,8 @@ -use rusqlite::{Connection, Result, params}; -use chrono::{Utc, DateTime}; +use chrono::{DateTime, Utc}; +use rusqlite::{params, Connection, Result}; use uuid::Uuid; -use crate::models::{StandSession, CycleConfig}; + +use crate::models::{CycleConfig, PersistedTimerState, StandSession}; pub fn get_db_path() -> String { let mut path = std::env::var_os("HOME") @@ -91,10 +92,7 @@ pub fn init_db() -> Result<()> { // Session operations -pub fn create_session( - user_id: &str, - device_id: &str, -) -> Result { +pub fn create_session(user_id: &str, device_id: &str) -> Result { let db_path = get_db_path(); let conn = Connection::open(&db_path)?; let id = Uuid::new_v4().to_string(); @@ -136,23 +134,20 @@ pub fn update_session_stand_start(session_id: &str) -> Result<()> { Ok(()) } -pub fn update_session_end( - session_id: &str, - end_source: &str, -) -> Result { +pub fn update_session_end(session_id: &str, end_source: &str) -> Result { let db_path = get_db_path(); let conn = Connection::open(&db_path)?; let now = Utc::now(); // Get actual_stand_start_at to calculate duration - let mut stmt = conn.prepare( - "SELECT actual_stand_start_at FROM stand_sessions WHERE id = ?1" - )?; + let mut stmt = + conn.prepare("SELECT actual_stand_start_at FROM stand_sessions WHERE id = ?1")?; let start_str: Option = stmt.query_row(&[session_id], |row| row.get(0))?; let duration = if let Some(start_str) = start_str { if let Ok(start_dt) = DateTime::parse_from_rfc3339(&start_str) { - let duration = now.signed_duration_since(start_dt) + let duration = now + .signed_duration_since(start_dt) .num_seconds() .max(0); Some(duration) @@ -179,6 +174,24 @@ pub fn update_session_end( Ok(duration.unwrap_or(0)) } +pub fn finish_or_discard_session(session_id: &str, end_source: &str) -> Result { + let db_path = get_db_path(); + let conn = Connection::open(&db_path)?; + let has_started: bool = conn.query_row( + "SELECT actual_stand_start_at IS NOT NULL FROM stand_sessions WHERE id = ?1", + [session_id], + |row| row.get(0), + )?; + + if has_started { + drop(conn); + update_session_end(session_id, end_source) + } else { + conn.execute("DELETE FROM stand_sessions WHERE id = ?1", [session_id])?; + Ok(0) + } +} + pub fn add_snooze_to_session(session_id: &str, snooze_minutes: i32) -> Result<()> { let db_path = get_db_path(); let conn = Connection::open(&db_path)?; @@ -204,7 +217,7 @@ pub fn get_session(session_id: &str) -> Result> { "SELECT id, user_id, device_id, scheduled_start_at, actual_stand_start_at, start_source, end_at, end_source, duration_sec, snooze_count, snooze_total_sec, created_at, updated_at - FROM stand_sessions WHERE id = ?1" + FROM stand_sessions WHERE id = ?1", )?; let mut rows = stmt.query(&[session_id])?; @@ -240,7 +253,7 @@ pub fn get_today_sessions(user_id: &str) -> Result> { duration_sec, snooze_count, snooze_total_sec, created_at, updated_at FROM stand_sessions WHERE user_id = ?1 AND date(end_at, 'localtime') = date('now', 'localtime') - ORDER BY end_at DESC" + ORDER BY end_at DESC", )?; let mut sessions = Vec::new(); @@ -278,7 +291,7 @@ pub fn get_or_create_config(user_id: &str) -> Result { let mut stmt = conn.prepare( "SELECT user_id, sit_minutes, stand_minutes, notifications_enabled, sound_enabled, auto_end_enabled, auto_end_after_sec, ui_skin, last_updated_at - FROM cycle_config WHERE user_id = ?1" + FROM cycle_config WHERE user_id = ?1", )?; if let Ok(row) = stmt.query_row(&[user_id], |row| { @@ -356,3 +369,53 @@ pub fn update_config(config: &CycleConfig) -> Result<()> { Ok(()) } + +pub fn save_timer_state(state: &PersistedTimerState) -> Result<()> { + let db_path = get_db_path(); + let conn = Connection::open(&db_path)?; + conn.execute( + "INSERT OR REPLACE INTO timer_state_persist ( + id, status, current_session_id, current_phase, + phase_remaining_sec, phase_start_sec, snooze_count, updated_at + ) VALUES ('main', ?1, ?2, ?3, ?4, ?5, 0, ?6)", + params![ + &state.status, + &state.current_session_id, + &state.current_phase, + state.phase_remaining_sec, + state.phase_start_sec, + &state.updated_at, + ], + )?; + Ok(()) +} + +pub fn load_timer_state() -> Result> { + let db_path = get_db_path(); + let conn = Connection::open(&db_path)?; + let mut stmt = conn.prepare( + "SELECT status, current_session_id, current_phase, + phase_remaining_sec, phase_start_sec, updated_at + FROM timer_state_persist WHERE id = 'main'", + )?; + let mut rows = stmt.query([])?; + let Some(row) = rows.next()? else { + return Ok(None); + }; + + Ok(Some(PersistedTimerState { + status: row.get(0)?, + current_session_id: row.get(1)?, + current_phase: row.get(2)?, + phase_remaining_sec: row.get(3)?, + phase_start_sec: row.get(4)?, + updated_at: row.get(5)?, + })) +} + +pub fn clear_timer_state() -> Result<()> { + let db_path = get_db_path(); + let conn = Connection::open(&db_path)?; + conn.execute("DELETE FROM timer_state_persist WHERE id = 'main'", [])?; + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index da03254..341386c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -12,7 +12,6 @@ use crate::timer::StandTimer; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() - .plugin(tauri_plugin_opener::init()) .setup(|app| { db::init_db().expect("failed to initialize StandForge database"); let config = db::get_or_create_config("default_user") @@ -21,10 +20,29 @@ pub fn run() { config.sit_minutes as i64, config.stand_minutes as i64, ))); - let session_id = db::create_session("default_user", "this_mac") - .expect("failed to start StandForge background session"); + let persisted = db::load_timer_state() + .expect("failed to load StandForge timer state"); if let Ok(timer_ref) = timer.lock() { - timer_ref.start_sitting(session_id); + timer_ref.set_auto_end( + config.auto_end_enabled, + config.auto_end_after_sec as i64, + ); + let restored = persisted + .as_ref() + .filter(|state| { + state.current_session_id.as_deref().is_some_and(|session_id| { + db::get_session(session_id).ok().flatten().is_some() + }) + }) + .is_some_and(|state| timer_ref.restore(state)); + + if !restored { + let session_id = db::create_session("default_user", "this_mac") + .expect("failed to start StandForge background session"); + timer_ref.start_sitting(session_id); + db::save_timer_state(&timer_ref.snapshot()) + .expect("failed to persist StandForge background session"); + } } app.manage(commands::AppState { diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 06e304b..d475afd 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -29,3 +29,13 @@ pub struct CycleConfig { pub ui_skin: String, pub last_updated_at: String, } + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct PersistedTimerState { + pub status: String, + pub current_session_id: Option, + pub current_phase: String, + pub phase_remaining_sec: i64, + pub phase_start_sec: i64, + pub updated_at: String, +} diff --git a/src-tauri/src/timer.rs b/src-tauri/src/timer.rs index 03560f1..5c624f8 100644 --- a/src-tauri/src/timer.rs +++ b/src-tauri/src/timer.rs @@ -1,22 +1,11 @@ -use std::sync::{Arc, Mutex}; use std::process::Command; -use tauri::{Emitter, Manager}; +use std::sync::{Arc, Mutex}; + use chrono::DateTime; -use crate::db; +use tauri::{Emitter, Manager}; -#[derive(Debug, Clone, serde::Serialize)] -pub enum TimerEvent { - TimerTick { - status: String, - current_phase: String, - remaining_seconds: i64, - total_phase_seconds: i64, - }, - PhaseComplete { - phase: String, - next_phase: String, - }, -} +use crate::db; +use crate::models::PersistedTimerState; #[derive(Debug, Clone, Copy, PartialEq)] pub enum TimerState { @@ -45,6 +34,18 @@ impl TimerState { TimerState::Paused => "paused", } } + + fn from_str(value: &str) -> Option { + match value { + "idle" => Some(Self::Idle), + "sitting" => Some(Self::Sitting), + "stand_pending" => Some(Self::StandPending), + "standing" => Some(Self::Standing), + "snoozed" => Some(Self::Snoozed), + "paused" => Some(Self::Paused), + _ => None, + } + } } impl TimerPhase { @@ -54,6 +55,14 @@ impl TimerPhase { TimerPhase::Stand => "stand", } } + + fn from_str(value: &str) -> Option { + match value { + "sit" => Some(Self::Sit), + "stand" => Some(Self::Stand), + _ => None, + } + } } #[derive(Debug)] @@ -68,6 +77,8 @@ pub struct StandTimer { paused_state: Arc>>, paused_remaining_sec: Arc>, phase_complete_emitted: Arc>, + auto_end_enabled: Arc>, + auto_end_after_sec: Arc>, } impl StandTimer { @@ -83,6 +94,8 @@ impl StandTimer { paused_state: Arc::new(Mutex::new(None)), paused_remaining_sec: Arc::new(Mutex::new(0)), phase_complete_emitted: Arc::new(Mutex::new(false)), + auto_end_enabled: Arc::new(Mutex::new(false)), + auto_end_after_sec: Arc::new(Mutex::new(3_600)), } } @@ -108,6 +121,12 @@ impl StandTimer { *self.phase_duration_sec.lock().unwrap() } + pub fn get_elapsed_seconds(&self) -> i64 { + (chrono::Utc::now() - *self.phase_start.lock().unwrap()) + .num_seconds() + .max(0) + } + pub fn get_current_session_id(&self) -> Option { self.current_session_id.lock().unwrap().clone() } @@ -117,6 +136,63 @@ impl StandTimer { *self.stand_duration_sec.lock().unwrap() = stand_minutes.max(1) * 60; } + pub fn set_auto_end(&self, enabled: bool, after_seconds: i64) { + *self.auto_end_enabled.lock().unwrap() = enabled; + *self.auto_end_after_sec.lock().unwrap() = after_seconds.max(60); + } + + pub fn snapshot(&self) -> PersistedTimerState { + PersistedTimerState { + status: self.get_state().as_str().to_string(), + current_session_id: self.get_current_session_id(), + current_phase: self.get_current_phase().as_str().to_string(), + phase_remaining_sec: self.get_remaining_seconds(), + phase_start_sec: chrono::Utc::now().timestamp(), + updated_at: chrono::Utc::now().to_rfc3339(), + } + } + + pub fn restore(&self, persisted: &PersistedTimerState) -> bool { + let Some(state) = TimerState::from_str(&persisted.status) else { + return false; + }; + let Some(phase) = TimerPhase::from_str(&persisted.current_phase) else { + return false; + }; + if state == TimerState::Idle || persisted.current_session_id.is_none() { + return false; + } + + let elapsed = if state == TimerState::Paused { + 0 + } else { + (chrono::Utc::now().timestamp() - persisted.phase_start_sec).max(0) + }; + let remaining = (persisted.phase_remaining_sec - elapsed).max(0); + + *self.state.lock().unwrap() = state; + *self.current_phase.lock().unwrap() = phase; + *self.phase_start.lock().unwrap() = + chrono::Utc::now() - chrono::Duration::seconds(elapsed); + *self.phase_duration_sec.lock().unwrap() = persisted.phase_remaining_sec.max(0); + *self.current_session_id.lock().unwrap() = persisted.current_session_id.clone(); + *self.paused_state.lock().unwrap() = if state == TimerState::Paused { + Some(match phase { + TimerPhase::Sit => TimerState::Sitting, + TimerPhase::Stand => TimerState::Standing, + }) + } else { + None + }; + *self.paused_remaining_sec.lock().unwrap() = if state == TimerState::Paused { + remaining + } else { + 0 + }; + *self.phase_complete_emitted.lock().unwrap() = false; + true + } + pub fn start_sitting(&self, session_id: String) { *self.state.lock().unwrap() = TimerState::Sitting; *self.current_phase.lock().unwrap() = TimerPhase::Sit; @@ -146,16 +222,6 @@ impl StandTimer { *self.phase_complete_emitted.lock().unwrap() = false; } - pub fn confirm_sit(&self) { - *self.state.lock().unwrap() = TimerState::Idle; - *self.current_session_id.lock().unwrap() = None; - *self.current_phase.lock().unwrap() = TimerPhase::Sit; - *self.phase_duration_sec.lock().unwrap() = 0; - *self.paused_state.lock().unwrap() = None; - *self.paused_remaining_sec.lock().unwrap() = 0; - *self.phase_complete_emitted.lock().unwrap() = false; - } - pub fn snooze(&self, snooze_minutes: i64) { let snooze_sec = snooze_minutes * 60; let new_duration = *self.phase_duration_sec.lock().unwrap() + snooze_sec; @@ -209,16 +275,6 @@ impl StandTimer { *self.phase_complete_emitted.lock().unwrap() = false; } - pub fn switch_to_sit(&self) { - *self.state.lock().unwrap() = TimerState::Sitting; - *self.current_phase.lock().unwrap() = TimerPhase::Sit; - *self.phase_start.lock().unwrap() = chrono::Utc::now(); - *self.phase_duration_sec.lock().unwrap() = *self.sit_duration_sec.lock().unwrap(); - *self.paused_state.lock().unwrap() = None; - *self.paused_remaining_sec.lock().unwrap() = 0; - *self.phase_complete_emitted.lock().unwrap() = false; - } - pub fn update(&self, app: &tauri::AppHandle) -> bool { let state = self.get_state(); if state == TimerState::Idle { @@ -240,6 +296,34 @@ impl StandTimer { return true; } + if state == TimerState::Standing { + let auto_end_enabled = *self.auto_end_enabled.lock().unwrap(); + let auto_end_after_sec = *self.auto_end_after_sec.lock().unwrap(); + if auto_end_enabled && self.get_elapsed_seconds() >= auto_end_after_sec { + if let Some(session_id) = self.get_current_session_id() { + let _ = db::update_session_end(&session_id, "auto_end"); + } + if let Ok(next_session_id) = db::create_session("default_user", "this_mac") { + self.start_next_sitting(next_session_id); + let _ = db::save_timer_state(&self.snapshot()); + } else { + self.stop(); + let _ = db::clear_timer_state(); + } + show_timer_notification( + "站立已自动结束", + "已达到自动结束时长", + "下一轮屏幕使用计时已开始。", + ); + app.emit_to("floating", "phase-complete", serde_json::json!({ + "phase": "stand", + "next_phase": "sit", + "auto_ended": true + })).ok(); + return true; + } + } + // Check if phase is complete if remaining <= 0 { let mut emitted = self.phase_complete_emitted.lock().unwrap(); @@ -252,6 +336,7 @@ impl StandTimer { match state { TimerState::Sitting => { self.transition_to_stand_pending(); + let _ = db::save_timer_state(&self.snapshot()); show_timer_notification( "站立提醒", "屏幕使用时间已到", @@ -278,6 +363,7 @@ impl StandTimer { TimerState::Snoozed => { // Return to pending state after snooze self.transition_to_stand_pending(); + let _ = db::save_timer_state(&self.snapshot()); show_timer_notification( "站立提醒", "延后时间到了", @@ -339,3 +425,65 @@ fn show_floating_window(app: &tauri::AppHandle) { let _ = window.set_focus(); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn start_pause_resume_and_stop_preserve_expected_state() { + let timer = StandTimer::new(45, 15); + timer.start_sitting("session-1".to_string()); + assert_eq!(timer.get_state(), TimerState::Sitting); + assert_eq!(timer.get_current_phase(), TimerPhase::Sit); + assert_eq!(timer.get_current_session_id().as_deref(), Some("session-1")); + + timer.pause(); + let paused_remaining = timer.get_remaining_seconds(); + assert_eq!(timer.get_state(), TimerState::Paused); + + timer.resume(); + assert_eq!(timer.get_state(), TimerState::Sitting); + assert!(timer.get_remaining_seconds() <= paused_remaining); + + timer.stop(); + assert_eq!(timer.get_state(), TimerState::Idle); + assert_eq!(timer.get_current_session_id(), None); + } + + #[test] + fn restore_uses_wall_clock_elapsed_time() { + let timer = StandTimer::new(45, 15); + let persisted = PersistedTimerState { + status: "sitting".to_string(), + current_session_id: Some("session-2".to_string()), + current_phase: "sit".to_string(), + phase_remaining_sec: 10, + phase_start_sec: chrono::Utc::now().timestamp() - 5, + updated_at: chrono::Utc::now().to_rfc3339(), + }; + + assert!(timer.restore(&persisted)); + assert_eq!(timer.get_state(), TimerState::Sitting); + assert!((4..=5).contains(&timer.get_remaining_seconds())); + } + + #[test] + fn paused_restore_does_not_consume_elapsed_time() { + let timer = StandTimer::new(45, 15); + let persisted = PersistedTimerState { + status: "paused".to_string(), + current_session_id: Some("session-3".to_string()), + current_phase: "stand".to_string(), + phase_remaining_sec: 120, + phase_start_sec: chrono::Utc::now().timestamp() - 300, + updated_at: chrono::Utc::now().to_rfc3339(), + }; + + assert!(timer.restore(&persisted)); + assert_eq!(timer.get_state(), TimerState::Paused); + assert_eq!(timer.get_remaining_seconds(), 120); + timer.resume(); + assert_eq!(timer.get_state(), TimerState::Standing); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 8ae973e..3ad3405 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -37,14 +37,18 @@ } ], "security": { - "csp": null + "csp": "default-src 'self'; connect-src ipc: http://ipc.localhost; font-src 'self' data:; img-src 'self' asset: http://asset.localhost data:; style-src 'self'; script-src 'self'" }, "macOSPrivateApi": true, - "withGlobalTauri": true + "withGlobalTauri": false }, "bundle": { "active": true, "targets": ["app"], + "macOS": { + "signingIdentity": "-", + "hardenedRuntime": true + }, "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 85f7a4a..0000000 --- a/src/App.css +++ /dev/null @@ -1,116 +0,0 @@ -.logo.vite:hover { - filter: drop-shadow(0 0 2em #747bff); -} - -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafb); -} -:root { - font-family: Inter, Avenir, Helvetica, Arial, sans-serif; - font-size: 16px; - line-height: 24px; - font-weight: 400; - - color: #0f0f0f; - background-color: #f6f6f6; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; -} - -.container { - margin: 0; - padding-top: 10vh; - display: flex; - flex-direction: column; - justify-content: center; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: 0.75s; -} - -.logo.tauri:hover { - filter: drop-shadow(0 0 2em #24c8db); -} - -.row { - display: flex; - justify-content: center; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} - -a:hover { - color: #535bf2; -} - -h1 { - text-align: center; -} - -input, -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - color: #0f0f0f; - background-color: #ffffff; - transition: border-color 0.25s; - box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); -} - -button { - cursor: pointer; -} - -button:hover { - border-color: #396cd8; -} -button:active { - border-color: #396cd8; - background-color: #e8e8e8; -} - -input, -button { - outline: none; -} - -#greet-input { - margin-right: 5px; -} - -@media (prefers-color-scheme: dark) { - :root { - color: #f6f6f6; - background-color: #2f2f2f; - } - - a:hover { - color: #24c8db; - } - - input, - button { - color: #ffffff; - background-color: #0f0f0f98; - } - button:active { - background-color: #0f0f0f69; - } -} diff --git a/src/App.tsx b/src/App.tsx index 28fbf86..c962bd2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,10 +8,19 @@ function App() { useEffect(() => { document.body.classList.add('is-floating'); document.documentElement.classList.add('is-floating'); + const colorScheme = window.matchMedia('(prefers-color-scheme: dark)'); + const applyColorScheme = (isDark: boolean) => { + document.documentElement.classList.toggle('dark', isDark); + }; + applyColorScheme(colorScheme.matches); + const handleColorSchemeChange = (event: MediaQueryListEvent) => applyColorScheme(event.matches); + colorScheme.addEventListener('change', handleColorSchemeChange); return () => { + colorScheme.removeEventListener('change', handleColorSchemeChange); document.body.classList.remove('is-floating'); document.documentElement.classList.remove('is-floating'); + document.documentElement.classList.remove('dark'); }; }, []); @@ -21,9 +30,19 @@ function App() { const syncInterval = window.setInterval(() => { void syncState(); - }, 1000); + }, 30_000); + + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') { + void syncState(); + } + }; + document.addEventListener('visibilitychange', handleVisibilityChange); - return () => window.clearInterval(syncInterval); + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + window.clearInterval(syncInterval); + }; }, [syncState]); return ; diff --git a/src/components/FloatingWindow.tsx b/src/components/FloatingWindow.tsx index 8d6c360..1b7334d 100644 --- a/src/components/FloatingWindow.tsx +++ b/src/components/FloatingWindow.tsx @@ -8,6 +8,8 @@ import { Check, ChevronDown, Clock3, + LoaderCircle, + LogOut, Pause, Palette, Play, @@ -17,6 +19,7 @@ import { Square, TimerReset, UserCheck, + Volume2, } from 'lucide-react'; import { DEFAULT_SIT_MINUTES, DEFAULT_STAND_MINUTES, DEFAULT_USER_ID, SNOOZE_OPTIONS } from '../lib/constants'; import type { StandSession, TodayStats } from '../lib/types'; @@ -109,6 +112,8 @@ export function FloatingWindow() { const [standMinutes, setStandMinutes] = useState(DEFAULT_STAND_MINUTES); const [todayStats, setTodayStats] = useState(null); const [todaySessions, setTodaySessions] = useState([]); + const [isActionPending, setIsActionPending] = useState(false); + const [actionError, setActionError] = useState(null); useEffect(() => { void loadConfig(); @@ -152,6 +157,8 @@ export function FloatingWindow() { const safeRemaining = Math.max(0, remainingSeconds); const uiSkin = config?.ui_skin ?? 'liquid_glass'; const notificationsEnabled = config?.notifications_enabled ?? true; + const soundEnabled = config?.sound_enabled ?? true; + const autoEndEnabled = config?.auto_end_enabled ?? false; const isIdle = status === 'idle'; const isPaused = status === 'paused'; const isStandPrompt = status === 'stand_pending' || status === 'snoozed'; @@ -185,7 +192,7 @@ export function FloatingWindow() { async (nextSitMinutes: number, nextStandMinutes: number) => { setSitMinutes(nextSitMinutes); setStandMinutes(nextStandMinutes); - await updateConfig({ + return updateConfig({ sit_minutes: nextSitMinutes, stand_minutes: nextStandMinutes, }); @@ -213,25 +220,45 @@ export function FloatingWindow() { } }, []); - const handlePrimaryAction = async () => { - if (isIdle) { - await handleDurationsCommit(sitMinutes, standMinutes); - await startTimer(); + const runAction = useCallback(async (action: () => Promise) => { + if (isActionPending) { return; } - if (isStandPrompt) { - await confirmStand(); - return; - } - if (isStanding) { - await confirmSit(); - return; - } - if (isPaused) { - await resumeTimer(); - return; + setIsActionPending(true); + setActionError(null); + try { + await action(); + } catch (error) { + setActionError(String(error)); + } finally { + setIsActionPending(false); } - await pauseTimer(); + }, [isActionPending]); + + const handlePrimaryAction = () => { + void runAction(async () => { + if (isIdle) { + const saved = await handleDurationsCommit(sitMinutes, standMinutes); + if (!saved) { + throw new Error('设置未保存,未启动计时器'); + } + await startTimer(); + return; + } + if (isStandPrompt) { + await confirmStand(); + return; + } + if (isStanding) { + await confirmSit(); + return; + } + if (isPaused) { + await resumeTimer(); + return; + } + await pauseTimer(); + }); }; const primaryLabel = isIdle @@ -282,8 +309,12 @@ export function FloatingWindow() { className="floating-icon-btn floating-primary-btn" onClick={handlePrimaryAction} aria-label={primaryLabel} + aria-busy={isActionPending} + disabled={isActionPending} > - {isIdle ? ( + {isActionPending ? ( + + ) : isIdle ? ( ) : isStandPrompt ? ( @@ -336,11 +367,14 @@ export function FloatingWindow() { @@ -377,7 +412,8 @@ export function FloatingWindow() { variant="outline" size="sm" className="floating-chip-button" - onClick={stopTimer} + onClick={() => void runAction(stopTimer)} + disabled={isActionPending || isIdle} > 结束 @@ -388,12 +424,17 @@ export function FloatingWindow() { variant="outline" size="sm" className="floating-chip-button" - onClick={switchToStand} + onClick={() => void runAction(switchToStand)} + disabled={isActionPending} > 现在站立 )} + + {actionError && ( +

操作失败:{actionError}

+ )} @@ -415,6 +456,41 @@ export function FloatingWindow() { /> +
+
+ +
+

提醒声音

+ 系统通知到达时播放提示音 +
+
+ void updateConfig({ sound_enabled: checked })} + aria-label="提醒声音" + /> +
+ +
+
+ +
+

自动结束站立

+ 站立 60 分钟后自动开始下一轮 +
+
+ void updateConfig({ + auto_end_enabled: checked, + auto_end_after_sec: 3600, + })} + aria-label="自动结束站立" + /> +
+
@@ -486,8 +562,19 @@ export function FloatingWindow() {
{configError && ( -

设置未保存:{configError}

+

设置未保存:{configError}

)} + +
diff --git a/src/components/ReminderWindow.tsx b/src/components/ReminderWindow.tsx deleted file mode 100644 index 1d08192..0000000 --- a/src/components/ReminderWindow.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { useMemo } from 'react'; -import { getCurrentWindow } from '@tauri-apps/api/window'; -import { Activity, Armchair, Clock3, Monitor, Settings2, UserCheck } from 'lucide-react'; -import { Button } from './ui/button'; -import { SNOOZE_OPTIONS } from '../lib/constants'; -import { useTimerStore } from '../stores/useTimerStore'; -import { invoke } from '@tauri-apps/api/core'; - -function formatTime(seconds: number) { - const mins = Math.floor(Math.max(0, seconds) / 60); - const secs = Math.max(0, seconds) % 60; - return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; -} - -async function hideReminder() { - try { - await getCurrentWindow().hide(); - } catch { - // The browser preview cannot hide a Tauri window. - } -} - -export function ReminderWindow() { - const { - status, - remainingSeconds, - totalPhaseSeconds, - confirmStand, - confirmSit, - snooze, - } = useTimerStore(); - - const isStandComplete = status === 'standing' && totalPhaseSeconds > 0 && remainingSeconds <= 0; - const isStandReminder = status === 'stand_pending' || status === 'snoozed'; - - const copy = useMemo(() => { - if (isStandComplete) { - return { - icon: Armchair, - title: '可以坐下了', - body: '这段站立已经完成。确认坐下后,StandForge 会继续在后台记录下一轮屏幕使用时间。', - timeLabel: '本轮站立完成', - }; - } - - return { - icon: Monitor, - title: '屏幕用久了,站一会儿', - body: '先离开椅子,伸展肩颈和背部。点“我已站起”后开始记录站立时间。', - timeLabel: status === 'snoozed' ? '延后已结束' : '屏幕使用时间已到', - }; - }, [isStandComplete, status]); - - const Icon = copy.icon; - - const handleConfirmStand = async () => { - await confirmStand(); - await hideReminder(); - }; - - const handleConfirmSit = async () => { - await confirmSit(); - await hideReminder(); - }; - - const handleSnooze = async (minutes: number) => { - await snooze(minutes); - await hideReminder(); - }; - - const handleOpenSettings = async () => { - try { - await invoke('show_window'); - } catch { - // Browser preview has no Tauri command bridge. - } - }; - - return ( -
-
-
-
-
- -
-
-

StandForge 提醒

-

{copy.title}

-
-
- -
- -
-
- -

{copy.timeLabel}

-
- {!isStandReminder && !isStandComplete && ( -

- {formatTime(remainingSeconds)} -

- )} -

{copy.body}

-
- -
- {isStandComplete ? ( - - ) : ( - <> - -
- {SNOOZE_OPTIONS.map((minutes) => ( - - ))} -
- - )} -
- - -
-
- ); -} diff --git a/src/components/TimerDisplay.tsx b/src/components/TimerDisplay.tsx deleted file mode 100644 index 9e82a0a..0000000 --- a/src/components/TimerDisplay.tsx +++ /dev/null @@ -1,459 +0,0 @@ -import { useMemo } from 'react'; -import { useTimerStore } from '../stores/useTimerStore'; -import { Button } from './ui/button'; -import { Slider } from './ui/slider'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs'; -import { SNOOZE_OPTIONS } from '../lib/constants'; -import type { StandSession, TodayStats } from '../lib/types'; -import { - Armchair, - BarChart3, - Clock3, - Pause, - Play, - RotateCcw, - Settings2, - Square, - TimerReset, - UserCheck, -} from 'lucide-react'; - -function formatTime(seconds: number) { - const mins = Math.floor(seconds / 60); - const secs = seconds % 60; - return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; -} - -function formatDuration(seconds: number) { - const safeSeconds = Math.max(0, Math.floor(seconds)); - const minutes = Math.floor(safeSeconds / 60); - const hours = Math.floor(minutes / 60); - const restMinutes = minutes % 60; - - if (hours > 0 && restMinutes > 0) { - return `${hours} 小时 ${restMinutes} 分`; - } - if (hours > 0) { - return `${hours} 小时`; - } - if (minutes > 0) { - return `${minutes} 分`; - } - return `${safeSeconds} 秒`; -} - -function formatSessionTime(value: string | null) { - if (!value) { - return '未记录'; - } - - const date = new Date(value); - if (Number.isNaN(date.getTime())) { - return '未记录'; - } - - return new Intl.DateTimeFormat('zh-CN', { - hour: '2-digit', - minute: '2-digit', - }).format(date); -} - -interface TimerDisplayProps { - sitMinutes: number; - standMinutes: number; - todayStats: TodayStats | null; - todaySessions: StandSession[]; - isConfigLoading: boolean; - configError: string | null; - onSitMinutesChange: (value: number) => void; - onStandMinutesChange: (value: number) => void; - onDurationsCommit: (sitMinutes: number, standMinutes: number) => Promise | void; - onRefreshStats: () => Promise | void; -} - -export function TimerDisplay({ - sitMinutes, - standMinutes, - todayStats, - todaySessions, - isConfigLoading, - configError, - onSitMinutesChange, - onStandMinutesChange, - onDurationsCommit, - onRefreshStats, -}: TimerDisplayProps) { - const { - status, - currentPhase, - remainingSeconds, - totalPhaseSeconds, - startTimer, - pauseTimer, - resumeTimer, - stopTimer, - switchToStand, - confirmStand, - confirmSit, - snooze, - } = useTimerStore(); - - const safeRemaining = Math.max(0, remainingSeconds); - const progress = - totalPhaseSeconds > 0 ? (totalPhaseSeconds - safeRemaining) / totalPhaseSeconds : 0; - const progressDeg = Math.min(1, Math.max(0, progress)) * 360; - - const isIdle = status === 'idle'; - const isStandPending = status === 'stand_pending' || status === 'snoozed'; - const isStanding = status === 'standing'; - const isStandComplete = isStanding && totalPhaseSeconds > 0 && safeRemaining === 0; - const isPaused = status === 'paused'; - - const phaseLabel = - status === 'stand_pending' || status === 'snoozed' - ? '等待站起' - : currentPhase === 'sit' - ? '屏幕使用' - : '站立'; - const switchToStandNow = - status === 'stand_pending' || status === 'snoozed' || currentPhase === 'sit'; - const phaseTotalLabel = - totalPhaseSeconds > 0 - ? `本轮 ${formatTime(totalPhaseSeconds)}` - : ''; - const statusLabel = isIdle - ? '准备开始' - : isPaused - ? '已暂停' - : status === 'snoozed' - ? '已延后' - : isStandPending - ? '等待站起' - : isStandComplete - ? '站立完成' - : `${phaseLabel}中`; - const metaLabel = isIdle - ? `屏幕 ${sitMinutes} 分 / 站立 ${standMinutes} 分` - : phaseTotalLabel || '后台提醒运行中'; - const latestSessions = useMemo(() => todaySessions.slice(0, 5), [todaySessions]); - - const stats = todayStats ?? { - total_duration_sec: 0, - session_count: 0, - snooze_count: 0, - snooze_total_sec: 0, - completion_rate: 0, - target_stand_sec: standMinutes * 60, - }; - - const handleStart = async () => { - await onDurationsCommit(sitMinutes, standMinutes); - await startTimer(); - }; - - const handleSitCommit = ([value]: number[]) => { - void onDurationsCommit(value, standMinutes); - }; - - const handleStandCommit = ([value]: number[]) => { - void onDurationsCommit(sitMinutes, value); - }; - - return ( -
- -
-
-
- - {statusLabel} -
-

{metaLabel}

-
- - - - 提醒 - - - - 设置 - - - - 今日 - - -
- - - {isIdle ? ( -
-
-

后台提醒节奏

-
-
-

屏幕使用

-

- {sitMinutes} - -

-
-
-

站立

-

- {standMinutes} - -

-
-
-
- - -

- 启动后会在后台记录屏幕使用时间,到点弹窗提醒。 -

-
- ) : ( -
-
-
-
-
-
- - {formatTime(safeRemaining)} - - - {isStandComplete ? '待确认' : '剩余'} - -
-
- - {isStandPending && ( -
-

该站起活动了

- -
- {SNOOZE_OPTIONS.map((minutes) => ( - - ))} -
-
- )} - - {isStanding && ( -
-

- {isStandComplete ? '站立完成,可以坐下' : '保持自然呼吸,重心放稳'} -

- -
- )} - -
- {isPaused ? ( - - ) : ( - - )} -
- - {switchToStandNow && ( - - )} -
-
-
- )} - - - -
-
-
-
- 屏幕使用时长 - {sitMinutes} 分钟 -
- onSitMinutesChange(value)} - onValueCommit={handleSitCommit} - min={5} - max={90} - step={5} - /> -
-
-
- 站立时长 - {standMinutes} 分钟 -
- onStandMinutesChange(value)} - onValueCommit={handleStandCommit} - min={3} - max={30} - step={1} - /> -
-
- -
-
- -
-

当前循环

-

- 屏幕使用 {sitMinutes} 分钟,站立 {standMinutes} 分钟。{isIdle ? '新设置会自动保存。' : '正在进行的阶段结束后生效。'} -

-
-
- {configError && ( -

- 设置未保存:{configError} -

- )} -
-
-
- - -
-
-

站立总时长

-

- {formatDuration(stats.total_duration_sec)} -

-
-
-

完成次数

-

- {stats.session_count} -

-
-
-

完成率

-

- {stats.completion_rate}% -

-
-
- -
-
-

今日记录

- -
- - {latestSessions.length > 0 ? ( -
- {latestSessions.map((session) => ( -
-
-
- -

- {formatSessionTime(session.actual_stand_start_at)} - {formatSessionTime(session.end_at)} -

-
-

- 延后 {session.snooze_count} 次,共 {formatDuration(session.snooze_total_sec)} -

-
-

- {formatDuration(session.duration_sec ?? 0)} -

-
- ))} -
- ) : ( -
-

今天还没有完成记录

-

完成一次站立后会出现在这里。

-
- )} -
-
- -
- ); -} diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx deleted file mode 100644 index 5748288..0000000 --- a/src/components/ui/card.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as React from "react" -import { cn } from "../../lib/utils" - -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -Card.displayName = "Card" - -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardHeader.displayName = "CardHeader" - -const CardTitle = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardTitle.displayName = "CardTitle" - -const CardDescription = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardDescription.displayName = "CardDescription" - -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardContent.displayName = "CardContent" - -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardFooter.displayName = "CardFooter" - -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/index.css b/src/index.css index 0267c22..80dfa13 100644 --- a/src/index.css +++ b/src/index.css @@ -1,9 +1,9 @@ -@import url("https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap"); @import "tailwindcss"; @config "../tailwind.config.js"; @layer base { :root { + color-scheme: light; /* Minimal: warm canvas + graphite text + calm teal accent */ --background: 36 33% 97%; --foreground: 220 20% 12%; @@ -26,11 +26,12 @@ --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06); --shadow-md: 0 12px 30px rgba(15, 23, 42, 0.08); --shadow-lg: 0 24px 60px rgba(15, 23, 42, 0.12); - --font-sans: "Manrope", "SF Pro Text", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif; - --font-display: "Space Grotesk", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif; + --font-sans: "SF Pro Text", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif; + --font-display: "SF Pro Rounded", "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif; } .dark { + color-scheme: dark; --background: 220 22% 8%; --foreground: 210 20% 94%; --card: 220 20% 10%; @@ -165,7 +166,7 @@ html.is-floating { background: rgba(255, 255, 255, 0.88); border: 1px solid rgba(15, 23, 42, 0.08); box-shadow: 0 24px 60px rgba(15, 23, 42, 0.22), 0 1px 0 rgba(255, 255, 255, 0.9) inset; - backdrop-filter: blur(24px); + backdrop-filter: blur(20px); } .dark .reminder-card { @@ -215,8 +216,8 @@ html.is-floating { background: rgba(255, 255, 255, 0.9); border: 1px solid rgba(15, 23, 42, 0.1); box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.72) inset, 0 1px 2px rgba(15, 23, 42, 0.08); - backdrop-filter: blur(24px); - -webkit-backdrop-filter: blur(24px); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); cursor: grab; overflow: hidden; user-select: none; @@ -319,8 +320,8 @@ html.is-floating { } .floating-icon-btn { - height: 28px; - width: 28px; + height: 36px; + width: 36px; border-radius: 999px; display: inline-flex; align-items: center; @@ -331,15 +332,28 @@ html.is-floating { transition: background-color 0.15s ease, transform 0.15s ease; } -.floating-icon-btn:hover { - background: rgba(15, 23, 42, 0.1); - transform: translateY(-1px); -} - .floating-icon-btn:active { transform: translateY(0); } +.floating-icon-btn:disabled { + cursor: wait; + opacity: 0.62; +} + +.floating-icon-btn:focus-visible, +.floating-skin-option:focus-visible { + outline: 2px solid hsl(var(--ring)); + outline-offset: 2px; +} + +@media (hover: hover) and (pointer: fine) { + .floating-icon-btn:hover { + background: rgba(15, 23, 42, 0.1); + transform: translateY(-1px); + } +} + .floating-primary-btn { color: hsl(var(--primary-foreground)); background: hsl(var(--primary)); @@ -748,8 +762,8 @@ html.is-floating { 0 0 0 1px rgba(255, 255, 255, 0.62) inset, 0 1px 0 rgba(255, 255, 255, 0.36) inset, 0 1px 2px rgba(15, 23, 42, 0.05); - backdrop-filter: blur(64px) saturate(2.35) brightness(1.08); - -webkit-backdrop-filter: blur(64px) saturate(2.35) brightness(1.08); + backdrop-filter: blur(20px) saturate(1.8) brightness(1.04); + -webkit-backdrop-filter: blur(20px) saturate(1.8) brightness(1.04); } .floating-skin-liquid-glass { @@ -799,8 +813,8 @@ html.is-floating { background: rgba(255, 255, 255, 0.14); border-color: rgba(255, 255, 255, 0.34); box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.24) inset; - backdrop-filter: blur(34px) saturate(1.9); - -webkit-backdrop-filter: blur(34px) saturate(1.9); + backdrop-filter: blur(12px) saturate(1.5); + -webkit-backdrop-filter: blur(12px) saturate(1.5); } .floating-skin-liquid-glass .floating-tabs-list { @@ -814,8 +828,8 @@ html.is-floating { background: rgba(255, 255, 255, 0.16); border-color: rgba(255, 255, 255, 0.36); box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.22) inset; - backdrop-filter: blur(24px) saturate(1.7); - -webkit-backdrop-filter: blur(24px) saturate(1.7); + backdrop-filter: blur(10px) saturate(1.4); + -webkit-backdrop-filter: blur(10px) saturate(1.4); } .floating-skin-liquid-glass .floating-primary-btn, @@ -871,8 +885,8 @@ html.is-floating { border: 1px solid var(--floating-glass-border); background: var(--floating-glass-surface); box-shadow: var(--floating-control-shadow); - backdrop-filter: blur(30px) saturate(1.65); - -webkit-backdrop-filter: blur(30px) saturate(1.65); + backdrop-filter: blur(12px) saturate(1.4); + -webkit-backdrop-filter: blur(12px) saturate(1.4); } .floating-card-expanded .floating-header { @@ -892,8 +906,8 @@ html.is-floating { border: 1px solid var(--floating-glass-border); background: var(--floating-glass-subtle); box-shadow: var(--floating-control-shadow); - backdrop-filter: blur(28px) saturate(1.55); - -webkit-backdrop-filter: blur(28px) saturate(1.55); + backdrop-filter: blur(10px) saturate(1.35); + -webkit-backdrop-filter: blur(10px) saturate(1.35); } .floating-tabs-trigger { diff --git a/src/lib/types.ts b/src/lib/types.ts index 9534380..1794885 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -7,7 +7,7 @@ export type TimerState = | 'snoozed' | 'paused'; -export type EndSource = 'user_confirm' | 'auto_end'; +export type EndSource = 'user_confirm' | 'auto_end' | 'user_stop' | 'manual_switch'; // Stand Session export interface StandSession { diff --git a/src/stores/useConfigStore.test.ts b/src/stores/useConfigStore.test.ts new file mode 100644 index 0000000..3d51558 --- /dev/null +++ b/src/stores/useConfigStore.test.ts @@ -0,0 +1,43 @@ +import { invoke } from '@tauri-apps/api/core'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { type CycleConfig, useConfigStore } from './useConfigStore'; + +vi.mock('@tauri-apps/api/core', () => ({ + invoke: vi.fn(), +})); + +const invokeMock = vi.mocked(invoke); +const baseConfig: CycleConfig = { + user_id: 'default_user', + sit_minutes: 45, + stand_minutes: 15, + notifications_enabled: true, + sound_enabled: true, + auto_end_enabled: false, + auto_end_after_sec: 3_600, + ui_skin: 'liquid_glass', + last_updated_at: '2026-07-11T00:00:00Z', +}; + +describe('useConfigStore', () => { + beforeEach(() => { + invokeMock.mockReset(); + useConfigStore.setState({ config: baseConfig, isLoading: false, error: null }); + }); + + it('reports a successful persisted update', async () => { + invokeMock.mockResolvedValue(undefined); + + await expect(useConfigStore.getState().updateConfig({ sit_minutes: 50 })).resolves.toBe(true); + expect(useConfigStore.getState().config?.sit_minutes).toBe(50); + }); + + it('reverts optimistic state when persistence fails', async () => { + invokeMock.mockRejectedValue(new Error('database unavailable')); + + await expect(useConfigStore.getState().updateConfig({ sit_minutes: 50 })).resolves.toBe(false); + expect(useConfigStore.getState().config?.sit_minutes).toBe(45); + expect(useConfigStore.getState().error).toContain('database unavailable'); + }); +}); diff --git a/src/stores/useConfigStore.ts b/src/stores/useConfigStore.ts index 8850a22..31e1a93 100644 --- a/src/stores/useConfigStore.ts +++ b/src/stores/useConfigStore.ts @@ -23,7 +23,7 @@ interface ConfigStore { // Actions loadConfig: () => Promise; - updateConfig: (updates: Partial) => Promise; + updateConfig: (updates: Partial) => Promise; } export const useConfigStore = create((set, get) => ({ @@ -43,16 +43,18 @@ export const useConfigStore = create((set, get) => ({ updateConfig: async (updates) => { const current = get().config; - if (!current) return; + if (!current) return false; const updated = { ...current, ...updates }; set({ config: updated, error: null }); try { await invoke('update_config_command', { config: updated }); + return true; } catch (e) { // Revert on error set({ config: current, error: String(e) }); + return false; } }, })); diff --git a/src/stores/useTimerStore.test.ts b/src/stores/useTimerStore.test.ts new file mode 100644 index 0000000..ed67cbc --- /dev/null +++ b/src/stores/useTimerStore.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; + +import { normalizeStatus } from './useTimerStore'; + +describe('normalizeStatus', () => { + it('normalizes the legacy standpending value', () => { + expect(normalizeStatus('standpending')).toBe('stand_pending'); + }); + + it('keeps supported timer states unchanged', () => { + expect(normalizeStatus('sitting')).toBe('sitting'); + expect(normalizeStatus('standing')).toBe('standing'); + expect(normalizeStatus('paused')).toBe('paused'); + }); + + it('falls back to idle for unknown backend values', () => { + expect(normalizeStatus('corrupt-state')).toBe('idle'); + }); +}); diff --git a/src/stores/useTimerStore.ts b/src/stores/useTimerStore.ts index dedfedd..98d734e 100644 --- a/src/stores/useTimerStore.ts +++ b/src/stores/useTimerStore.ts @@ -36,7 +36,7 @@ interface TimerStatePayload { current_session_id?: string | null; } -function normalizeStatus(status: string): TimerState { +export function normalizeStatus(status: string): TimerState { switch (status) { case 'standpending': return 'stand_pending'; @@ -171,25 +171,9 @@ export function setupTimerListeners() { }); // Listen for phase complete events - listen<{ phase: string; next_phase: string }>('phase-complete', (event) => { + listen<{ phase: string; next_phase: string; auto_ended?: boolean }>('phase-complete', () => { const store = useTimerStore.getState(); - if (event.payload.phase === 'sit') { - store.hydrateState({ - status: 'stand_pending', - current_phase: 'stand', - remaining_seconds: 0, - total_phase_seconds: store.totalPhaseSeconds, - current_session_id: store.currentSessionId, - }); - } else if (event.payload.phase === 'stand') { - store.hydrateState({ - status: 'standing', - current_phase: 'stand', - remaining_seconds: 0, - total_phase_seconds: store.totalPhaseSeconds, - current_session_id: store.currentSessionId, - }); - } + void store.syncState(); }).catch(() => { listenersStarted = false; }); diff --git a/vite.config.ts b/vite.config.ts index 618ae71..18c2bcc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,6 +12,9 @@ export default defineConfig(async () => ({ // // 1. prevent Vite from obscuring rust errors clearScreen: false, + optimizeDeps: { + entries: ["index.html"], + }, // 2. tauri expects a fixed port, fail if that port is not available server: { port: 5173, @@ -26,7 +29,11 @@ export default defineConfig(async () => ({ : undefined, watch: { // 3. tell Vite to ignore watching `src-tauri` - ignored: ["**/src-tauri/**"], + ignored: [ + "**/src-tauri/**", + "**/native-macos/.build/**", + "**/native-macos/build/**", + ], }, }, })); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..48998a5 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['src/**/*.test.{ts,tsx}'], + exclude: [ + 'node_modules/**', + 'dist/**', + 'src-tauri/**', + 'native-macos/**', + ], + }, +});