From 19dc77d4ff8a03c7cb09e053f77825372d7a6ada Mon Sep 17 00:00:00 2001 From: Jared Henderson Date: Mon, 2 Oct 2023 12:13:00 -0400 Subject: [PATCH 01/25] macapp: onboarding feature roughed in --- .../PairQL/MacApp/Resolvers/ConnectUser.swift | 10 +- .../ConnectUserResolverTests.swift | 2 +- .../App/Sources/App/App+PersistantState.swift | 6 +- macapp/App/Sources/App/App.swift | 5 +- macapp/App/Sources/App/AppReducer.swift | 85 ++- .../App/Sources/App/AppUpdatesFeature.swift | 9 +- .../App/Sources/App/ApplicationFeature.swift | 69 +- .../DeviceClient/DeviceClient.swift | 19 + .../MonitoringClient/MonitoringClient.swift | 15 + .../App/Dependencies/SecurityClient.swift | 3 + .../App/Dependencies/StorageClient.swift | 6 + .../App/Dependencies/UpdaterClient.swift | 3 + macapp/App/Sources/App/FilterFeature.swift | 41 +- macapp/App/Sources/App/HistoryFeature.swift | 11 +- .../App/Onboarding/OnboardingFeature.swift | 401 ++++++++++ macapp/App/Sources/App/PqlError+Message.swift | 6 +- macapp/App/Sources/App/RequestState.swift | 75 ++ macapp/App/Sources/App/Types.swift | 4 + .../Sources/ClientInterfaces/ApiClient.swift | 32 +- .../Sources/ClientInterfaces/AppClient.swift | 9 + .../FilterExtensionClient.swift | 13 + .../ClientInterfaces/FilterXPCClient.swift | 17 + .../ClientInterfaces/WebSocketClient.swift | 8 + .../App/Sources/Core/UserDefaultsClient.swift | 6 + .../Filter/Dependencies/ExtensionClient.swift | 3 + .../Filter/Dependencies/SecurityClient.swift | 4 + .../Filter/Dependencies/StorageClient.swift | 7 + .../Filter/Dependencies/XPCClient.swift | 7 + .../SystemClient.swift | 13 + .../App/Tests/AppTests/AdminWindowTests.swift | 3 +- .../App/Tests/AppTests/AppMigratorTests.swift | 295 ++++---- .../App/Tests/AppTests/AppReducerTests.swift | 49 +- .../BlockedRequestsFeatureTests.swift | 5 +- .../Tests/AppTests/FilterFeatureTests.swift | 6 +- .../AppTests/HistoryUserConnectionTests.swift | 4 +- .../AppTests/MonitoringFeatureTests.swift | 8 +- .../AppTests/OnboardingFeatureTests.swift | 691 ++++++++++++++++++ .../App/Tests/AppTests/UserFeatureTests.swift | 5 +- .../FilterTests/FilterReducerTests.swift | 17 +- 39 files changed, 1696 insertions(+), 276 deletions(-) create mode 100644 macapp/App/Sources/App/Onboarding/OnboardingFeature.swift create mode 100644 macapp/App/Tests/AppTests/OnboardingFeatureTests.swift diff --git a/api/Sources/Api/PairQL/MacApp/Resolvers/ConnectUser.swift b/api/Sources/Api/PairQL/MacApp/Resolvers/ConnectUser.swift index bd0bf2c8..3b232126 100644 --- a/api/Sources/Api/PairQL/MacApp/Resolvers/ConnectUser.swift +++ b/api/Sources/Api/PairQL/MacApp/Resolvers/ConnectUser.swift @@ -8,7 +8,8 @@ extension ConnectUser: Resolver { else { throw context.error( id: "6e7fc234", type: .unauthorized, - debugMessage: "ConnectApp verification code not found", + debugMessage: "verification code not found", + userMessage: "Connection code not found, or expired. Please try again.", appTag: .connectionCodeNotFound ) } @@ -37,7 +38,12 @@ extension ConnectUser: Resolver { // sanity check - we only "transfer" a device, if the admin accounts match let existingUser = try await existingUserDevice.user() if existingUser.adminId != user.adminId { - throw Abort(.forbidden, reason: "Device already registered to another admin's user") + throw context.error( + id: "41a43089", + type: .unauthorized, + debugMessage: "invalid connect transfer attempt", + userMessage: "This user is associated with another Gertrude parent account." + ) } let oldUserId = existingUserDevice.userId diff --git a/api/Tests/ApiTests/MacappPairResolvers/ConnectUserResolverTests.swift b/api/Tests/ApiTests/MacappPairResolvers/ConnectUserResolverTests.swift index 6ccaa778..5960614f 100644 --- a/api/Tests/ApiTests/MacappPairResolvers/ConnectUserResolverTests.swift +++ b/api/Tests/ApiTests/MacappPairResolvers/ConnectUserResolverTests.swift @@ -114,7 +114,7 @@ final class ConnectUserResolversTests: ApiTestCase { try await expectErrorFrom { [self] in _ = try await ConnectUser.resolve(with: input, in: self.context) - }.toContain("registered to another admin") + }.toContain("associated with another Gertrude parent account") // old token is not deleted let retrievedOldToken = try? await Current.db.find(existingUserToken.id) diff --git a/macapp/App/Sources/App/App+PersistantState.swift b/macapp/App/Sources/App/App+PersistantState.swift index ae46d23b..3e5c0417 100644 --- a/macapp/App/Sources/App/App+PersistantState.swift +++ b/macapp/App/Sources/App/App+PersistantState.swift @@ -11,6 +11,9 @@ enum Persistent { var appUpdateReleaseChannel: ReleaseChannel var filterVersion: String var user: UserData? + + // added for v2.1.0, but is backwards compatible + var resumeOnboarding: OnboardingFeature.Resume? } // v2.0.0 - v2.0.3 @@ -28,7 +31,8 @@ extension AppReducer.State { appVersion: appUpdates.installedVersion, appUpdateReleaseChannel: appUpdates.releaseChannel, filterVersion: filter.version, - user: user.data + user: user.data, + resumeOnboarding: nil ) } } diff --git a/macapp/App/Sources/App/App.swift b/macapp/App/Sources/App/App.swift index 204d79be..c702e621 100644 --- a/macapp/App/Sources/App/App.swift +++ b/macapp/App/Sources/App/App.swift @@ -11,7 +11,10 @@ typealias UserData = GetUserData.Output var adminWindow: AdminWindow var requestSuspensionWindow: RequestSuspensionWindow let store = Store( - initialState: AppReducer.State(), + initialState: AppReducer.State(appVersion: { + @Dependency(\.app) var appClient + return appClient.installedVersion() + }()), reducer: { AppReducer()._printChanges(.filteredBy { action in switch action { diff --git a/macapp/App/Sources/App/AppReducer.swift b/macapp/App/Sources/App/AppReducer.swift index 995a0319..91871cfa 100644 --- a/macapp/App/Sources/App/AppReducer.swift +++ b/macapp/App/Sources/App/AppReducer.swift @@ -11,14 +11,20 @@ struct AppReducer: Reducer, Sendable { struct State: Equatable, Sendable { var admin = AdminFeature.State() var adminWindow = AdminWindowFeature.State() - var appUpdates = AppUpdatesFeature.State() + var appUpdates: AppUpdatesFeature.State var blockedRequests = BlockedRequestsFeature.State() - var filter = FilterFeature.State() + var filter: FilterFeature.State var history = HistoryFeature.State() var menuBar = MenuBarFeature.State() + var onboarding = OnboardingFeature.State() var monitoring = MonitoringFeature.State() var requestSuspension = RequestSuspensionFeature.State() var user = UserFeature.State() + + init(appVersion: String?) { + appUpdates = .init(installedVersion: appVersion) + filter = .init(appVersion: appVersion) + } } enum Action: Equatable, Sendable { @@ -43,11 +49,13 @@ struct AppReducer: Reducer, Sendable { case history(HistoryFeature.Action) case menuBar(MenuBarFeature.Action) case monitoring(MonitoringFeature.Action) + case onboarding(OnboardingFeature.Action) case loadedPersistentState(Persistent.State?) case user(UserFeature.Action) case heartbeat(Heartbeat.Interval) case blockedRequests(BlockedRequestsFeature.Action) case requestSuspension(RequestSuspensionFeature.Action) + case startHeartbeat case websocket(WebSocketFeature.Action) indirect case adminAuthed(Action) @@ -56,33 +64,72 @@ struct AppReducer: Reducer, Sendable { @Dependency(\.api) var api @Dependency(\.device) var device @Dependency(\.backgroundQueue) var bgQueue + @Dependency(\.network) var network + @Dependency(\.storage) var storage var body: some ReducerOf { Reduce { state, action in #if !DEBUG os_log("[G•] APP received action: %{public}@", String(describing: action)) #endif + switch action { - case .loadedPersistentState(.some(let persistent)): - state.appUpdates.releaseChannel = persistent.appUpdateReleaseChannel - state.filter.version = persistent.filterVersion - guard let user = persistent.user else { return .none } - state.user = .init(data: user) - return .exec { [filterVersion = state.filter.version] send in - await api.setUserToken(user.token) - try await bgQueue.sleep(for: .milliseconds(10)) // <- unit test determinism - return await send(.checkIn( - result: TaskResult { try await api.appCheckIn(filterVersion) }, - reason: .appLaunched - )) + case .loadedPersistentState(.none): + state.onboarding.windowOpen = true + return .exec { [new = state.persistent] _ in + try await storage.savePersistentState(new) } + case .loadedPersistentState(.some(let persisted)) where persisted.resumeOnboarding != nil: + return .merge( + .exec { send in + await send(.onboarding(.resume(persisted.resumeOnboarding ?? .at(step: .welcome)))) + }, + .exec { [withoutResume = state.persistent] _ in + try await storage.savePersistentState(withoutResume) + } + ) + + case .loadedPersistentState(.some(let persisted)): + state.appUpdates.releaseChannel = persisted.appUpdateReleaseChannel + state.filter.version = persisted.filterVersion + guard let user = persisted.user else { + // TODO: are we sure we want to start the heartbeat? + return .exec { send in await send(.startHeartbeat) } + } + state.user = .init(data: user) + return .merge( + .exec { send in + await send(.startHeartbeat) + }, + .exec { [filterVersion = state.filter.version] send in + await api.setUserToken(user.token) + guard network.isConnected() else { return } + await send(.checkIn( + result: TaskResult { try await api.appCheckIn(filterVersion) }, + reason: .appLaunched + )) + } + ) + + case .startHeartbeat: + return .exec { send in + var numTicks = 0 + for await _ in bgQueue.timer(interval: .seconds(60)) { + numTicks += 1 + for interval in heartbeatIntervals(for: numTicks) { + await send(.heartbeat(interval)) + } + } + }.cancellable(id: Heartbeat.CancelId.interval) + case .focusedNotification(let notification): // dismiss windows/dropdowns so notification is visible, i.e. "focused" state.adminWindow.windowOpen = false state.menuBar.dropdownOpen = false state.blockedRequests.windowOpen = false state.requestSuspension.windowOpen = false + state.onboarding.windowOpen = false return .exec { _ in switch notification { case .unexpectedError: @@ -92,6 +139,13 @@ struct AppReducer: Reducer, Sendable { } } + case .onboarding(.delegate(.saveCurrentStep(let step))): + return .exec { [persist = state.persistent] _ in + var copy = persist + copy.resumeOnboarding = step.map { .at(step: $0) } + try await storage.savePersistentState(copy) + } + default: return .none } @@ -144,5 +198,8 @@ struct AppReducer: Reducer, Sendable { Scope(state: \.user, action: /Action.user) { UserFeature.Reducer() } + Scope(state: \.onboarding, action: /Action.onboarding) { + OnboardingFeature.Reducer() + } } } diff --git a/macapp/App/Sources/App/AppUpdatesFeature.swift b/macapp/App/Sources/App/AppUpdatesFeature.swift index b360dd54..f22480c7 100644 --- a/macapp/App/Sources/App/AppUpdatesFeature.swift +++ b/macapp/App/Sources/App/AppUpdatesFeature.swift @@ -42,10 +42,9 @@ struct AppUpdatesFeature: Feature { } extension AppUpdatesFeature.State { - init() { - @Dependency(\.app) var appClient + init(installedVersion: String?) { self.init( - installedVersion: appClient.installedVersion() ?? "0.0.0", + installedVersion: installedVersion ?? "0.0.0", releaseChannel: .stable, latestVersion: nil ) @@ -56,10 +55,6 @@ extension AppUpdatesFeature.RootReducer: FilterControlling { func reduce(into state: inout State, action: Action) -> Effect { switch action { - case .loadedPersistentState(.none): - return .exec { [new = state.persistent] _ in - try await storage.savePersistentState(new) - } case .loadedPersistentState(.some(let restored)): guard restored.appVersion != state.appUpdates.installedVersion else { diff --git a/macapp/App/Sources/App/ApplicationFeature.swift b/macapp/App/Sources/App/ApplicationFeature.swift index 795b8be4..63e56235 100644 --- a/macapp/App/Sources/App/ApplicationFeature.swift +++ b/macapp/App/Sources/App/ApplicationFeature.swift @@ -29,19 +29,19 @@ extension ApplicationFeature.RootReducer: RootReducing { case .application(.didFinishLaunching): return .merge( - .exec { _ in - // requesting notification authorization at least once - // ensures that the system prefs panel will show Gertrude - // TODO: consider delaying this if no user connected - await device.requestNotificationAuthorization() - }, + // .exec { _ in + // // requesting notification authorization at least once + // // ensures that the system prefs panel will show Gertrude + // // TODO: consider delaying this if no user connected + // await device.requestNotificationAuthorization() + // }, .exec { send in await send(.loadedPersistentState(try await storage.loadPersistentState())) }, .exec { send in - try await bgQueue.sleep(for: .milliseconds(5)) // <- unit test determinism + // try await bgQueue.sleep(for: .milliseconds(5)) // <- unit test determinism let setupState = await filterExtension.setup() await send(.filter(.receivedState(setupState))) if setupState.installed { @@ -49,20 +49,21 @@ extension ApplicationFeature.RootReducer: RootReducing { } }, - .exec { send in - var numTicks = 0 - for await _ in bgQueue.timer(interval: .seconds(60)) { - numTicks += 1 - for interval in heartbeatIntervals(for: numTicks) { - await send(.heartbeat(interval)) - } - } - }.cancellable(id: Heartbeat.CancelId.interval), + // .exec { send in + // var numTicks = 0 + // for await _ in bgQueue.timer(interval: .seconds(60)) { + // numTicks += 1 + // for interval in heartbeatIntervals(for: numTicks) { + // await send(.heartbeat(interval)) + // } + // } + // }.cancellable(id: Heartbeat.CancelId.interval), .exec { _ in - if await app.isLaunchAtLoginEnabled() == false { - await app.enableLaunchAtLogin() - } + // TODO: should be part of onboarding... + // if await app.isLaunchAtLoginEnabled() == false { + // await app.enableLaunchAtLogin() + // } }, .publisher { @@ -91,21 +92,21 @@ extension ApplicationFeature.RootReducer: RootReducing { return .none } } +} - func heartbeatIntervals(for tick: Int) -> [Heartbeat.Interval] { - var intervals: [Heartbeat.Interval] = [.everyMinute] - if tick % 5 == 0 { - intervals.append(.everyFiveMinutes) - } - if tick % 20 == 0 { - intervals.append(.everyTwentyMinutes) - } - if tick % 60 == 0 { - intervals.append(.everyHour) - } - if tick % 360 == 0 { - intervals.append(.everySixHours) - } - return intervals +func heartbeatIntervals(for tick: Int) -> [Heartbeat.Interval] { + var intervals: [Heartbeat.Interval] = [.everyMinute] + if tick % 5 == 0 { + intervals.append(.everyFiveMinutes) + } + if tick % 20 == 0 { + intervals.append(.everyTwentyMinutes) + } + if tick % 60 == 0 { + intervals.append(.everyHour) + } + if tick % 360 == 0 { + intervals.append(.everySixHours) } + return intervals } diff --git a/macapp/App/Sources/App/Dependencies/DeviceClient/DeviceClient.swift b/macapp/App/Sources/App/Dependencies/DeviceClient/DeviceClient.swift index cb01fab8..301d6b45 100644 --- a/macapp/App/Sources/App/Dependencies/DeviceClient/DeviceClient.swift +++ b/macapp/App/Sources/App/Dependencies/DeviceClient/DeviceClient.swift @@ -40,6 +40,25 @@ extension DeviceClient: DependencyKey { extension DeviceClient: TestDependencyKey { static let testValue = Self( + currentMacOsUserType: unimplemented("DeviceClient.currentMacOsUserType"), + currentUserId: unimplemented("DeviceClient.currentUserId"), + fullUsername: unimplemented("DeviceClient.fullUsername"), + listMacOSUsers: unimplemented("DeviceClient.listMacOSUsers"), + modelIdentifier: unimplemented("DeviceClient.modelIdentifier"), + notificationsSetting: unimplemented("DeviceClient.notificationsSetting"), + numericUserId: unimplemented("DeviceClient.numericUserId"), + openSystemPrefs: unimplemented("DeviceClient.openSystemPrefs"), + openWebUrl: unimplemented("DeviceClient.openWebUrl"), + quitBrowsers: unimplemented("DeviceClient.quitBrowsers"), + requestNotificationAuthorization: unimplemented( + "DeviceClient.requestNotificationAuthorization" + ), + showNotification: unimplemented("DeviceClient.showNotification"), + serialNumber: unimplemented("DeviceClient.serialNumber"), + username: unimplemented("DeviceClient.username") + ) + + static let mock = Self( currentMacOsUserType: { .standard }, currentUserId: { 502 }, fullUsername: { "test-full-username" }, diff --git a/macapp/App/Sources/App/Dependencies/MonitoringClient/MonitoringClient.swift b/macapp/App/Sources/App/Dependencies/MonitoringClient/MonitoringClient.swift index d359169d..a06275e3 100644 --- a/macapp/App/Sources/App/Dependencies/MonitoringClient/MonitoringClient.swift +++ b/macapp/App/Sources/App/Dependencies/MonitoringClient/MonitoringClient.swift @@ -54,6 +54,21 @@ extension MonitoringClient: DependencyKey { extension MonitoringClient: TestDependencyKey { static let testValue = Self( + commitPendingKeystrokes: unimplemented("MonitoringClient.commitPendingKeystrokes"), + keystrokeRecordingPermissionGranted: unimplemented( + "MonitoringClient.keystrokeRecordingPermissionGranted" + ), + restorePendingKeystrokes: unimplemented("MonitoringClient.restorePendingKeystrokes"), + screenRecordingPermissionGranted: unimplemented( + "MonitoringClient.screenRecordingPermissionGranted" + ), + startLoggingKeystrokes: unimplemented("MonitoringClient.startLoggingKeystrokes"), + stopLoggingKeystrokes: unimplemented("MonitoringClient.stopLoggingKeystrokes"), + takePendingKeystrokes: unimplemented("MonitoringClient.takePendingKeystrokes"), + takePendingScreenshots: unimplemented("MonitoringClient.takePendingScreenshots"), + takeScreenshot: unimplemented("MonitoringClient.takeScreenshot") + ) + static let mock = Self( commitPendingKeystrokes: { _ in }, keystrokeRecordingPermissionGranted: { true }, restorePendingKeystrokes: { _ in }, diff --git a/macapp/App/Sources/App/Dependencies/SecurityClient.swift b/macapp/App/Sources/App/Dependencies/SecurityClient.swift index 2d60d1d9..929984ef 100644 --- a/macapp/App/Sources/App/Dependencies/SecurityClient.swift +++ b/macapp/App/Sources/App/Dependencies/SecurityClient.swift @@ -37,6 +37,9 @@ extension SecurityClient: DependencyKey { extension SecurityClient: TestDependencyKey { public static let testValue = Self( + didAuthenticateAsAdmin: unimplemented("SecurityClient.didAuthenticateAsAdmin") + ) + public static let mock = Self( didAuthenticateAsAdmin: { false } ) } diff --git a/macapp/App/Sources/App/Dependencies/StorageClient.swift b/macapp/App/Sources/App/Dependencies/StorageClient.swift index 652647fb..35863229 100644 --- a/macapp/App/Sources/App/Dependencies/StorageClient.swift +++ b/macapp/App/Sources/App/Dependencies/StorageClient.swift @@ -42,6 +42,12 @@ extension StorageClient: DependencyKey { extension StorageClient: TestDependencyKey { static let testValue = Self( + savePersistentState: unimplemented("StorageClient.savePersistentState"), + loadPersistentState: unimplemented("StorageClient.loadPersistentState"), + deleteAllPersistentState: unimplemented("StorageClient.deleteAllPersistentState"), + deleteAll: unimplemented("StorageClient.deleteAll") + ) + static let mock = Self( savePersistentState: { _ in }, loadPersistentState: { nil }, deleteAllPersistentState: {}, diff --git a/macapp/App/Sources/App/Dependencies/UpdaterClient.swift b/macapp/App/Sources/App/Dependencies/UpdaterClient.swift index 6d44a9bf..aa738534 100644 --- a/macapp/App/Sources/App/Dependencies/UpdaterClient.swift +++ b/macapp/App/Sources/App/Dependencies/UpdaterClient.swift @@ -12,6 +12,9 @@ public struct UpdaterClient: Sendable { extension UpdaterClient: TestDependencyKey { public static let testValue = Self( + triggerUpdate: unimplemented("UpdaterClient.triggerUpdate") + ) + public static let mock = Self( triggerUpdate: { _ in } ) } diff --git a/macapp/App/Sources/App/FilterFeature.swift b/macapp/App/Sources/App/FilterFeature.swift index e23b73bc..ae5313b4 100644 --- a/macapp/App/Sources/App/FilterFeature.swift +++ b/macapp/App/Sources/App/FilterFeature.swift @@ -47,12 +47,11 @@ struct FilterFeature: Feature { } extension FilterFeature.State { - init() { - @Dependency(\.app) var appClient + init(appVersion: String?) { self.init( currentSuspensionExpiration: nil, extension: .unknown, - version: appClient.installedVersion() ?? "unknown" + version: appVersion ?? "unknown" ) } @@ -143,37 +142,33 @@ extension FilterFeature.RootReducer { return .merge( .exec { send in if !extensionInstalled { - switch await filterExtension.install() { + let installResult = await filterExtension.install() + switch installResult { case .installedSuccessfully: break case .timedOutWaiting: // event `9ffabfe5` logged w/ more detail in FilterFeature.swift await send(.focusedNotification(.filterInstallTimeout)) case .userClickedDontAllow: + interestingEvent(id: "01f94ff3") await send(.focusedNotification(.filterInstallDenied)) - case .activationRequestFailed(let error): - unexpectedError(id: "61d0eda0", error) - case .failedToGetBundleIdentifier: - unexpectedError(id: "d4a652e9") - case .failedToLoadConfig: - unexpectedError(id: "bd04ba1a") - case .failedToSaveConfig: - unexpectedError(id: "161ed707") - case .alreadyInstalled: - unexpectedError(id: "ff51a770") + case .activationRequestFailed, + .failedToGetBundleIdentifier, + .failedToLoadConfig, + .failedToSaveConfig, + .alreadyInstalled: + unexpectedError(id: "8a8762e7", detail: "result: \(installResult)") } } else { - switch await filterExtension.start() { + let state = await filterExtension.start() + switch state { case .installedAndRunning: break - case .errorLoadingConfig: - unexpectedError(id: "c291bcef") - case .installedButNotRunning: - unexpectedError(id: "99f3465c") - case .notInstalled: - unexpectedError(id: "6e4f30ac") - case .unknown: - unexpectedError(id: "24f31d4c") + case .errorLoadingConfig, + .installedButNotRunning, + .notInstalled, + .unknown: + unexpectedError(id: "cb2a0564", detail: "state: \(state)") } } }, diff --git a/macapp/App/Sources/App/HistoryFeature.swift b/macapp/App/Sources/App/HistoryFeature.swift index ac9a051b..025e3587 100644 --- a/macapp/App/Sources/App/HistoryFeature.swift +++ b/macapp/App/Sources/App/HistoryFeature.swift @@ -52,7 +52,7 @@ extension HistoryFeature.RootReducer: RootReducing { state.history.userConnection = .connecting return .exec { send in await send(.history(.userConnection(.connect(TaskResult { - try await api.connectUser(connectUserInput(code: code)) + try await api.connectUser(.init(code: code, device: device, app: app)) })))) } @@ -73,7 +73,8 @@ extension HistoryFeature.RootReducer: RootReducing { state.history.userConnection = .established(welcomeDismissed: true) return .none - case .history(.userConnection(.connect(.success(let user)))): + case .history(.userConnection(.connect(.success(let user)))), + .onboarding(.connectUser(.success(let user))): state.user = .init(data: user) return .exec { [persistent = state.persistent] _ in try await storage.savePersistentState(persistent) @@ -89,13 +90,15 @@ extension HistoryFeature.RootReducer: RootReducing { return .none } } +} - private func connectUserInput(code: Int) throws -> ConnectUser.Input { +extension ConnectUser.Input { + init(code: Int, device: DeviceClient, app: AppClient) throws { guard let serialNumber = device.serialNumber() else { struct NoSerialNumber: Error {} throw NoSerialNumber() } - return ConnectUser.Input( + self.init( verificationCode: code, appVersion: app.installedVersion() ?? "unknown", modelIdentifier: device.modelIdentifier() ?? "unknown", diff --git a/macapp/App/Sources/App/Onboarding/OnboardingFeature.swift b/macapp/App/Sources/App/Onboarding/OnboardingFeature.swift new file mode 100644 index 00000000..1f7c91a2 --- /dev/null +++ b/macapp/App/Sources/App/Onboarding/OnboardingFeature.swift @@ -0,0 +1,401 @@ +import ComposableArchitecture +import Foundation + +struct OnboardingFeature: Feature { + struct State: Equatable, Encodable, Sendable { + struct MacUser: Equatable, Encodable { + var id: uid_t + var name: String + var isAdmin: Bool + + enum RemediationStep: String, Equatable, Encodable { + case create + case `switch` + case demote + case choose + } + } + + var windowOpen = false + var step: Step = .welcome + var userRemediationStep: MacUser.RemediationStep? + var currentUser: MacUser? + var existingNotificationsSetting: NotificationsSetting? + var connectChildRequest: PayloadRequestState = .idle + var users: [MacUser] = [] + } + + enum Resume: Codable, Equatable, Sendable { + case checkingScreenRecordingPermission + case at(step: State.Step) + } + + enum Action: Equatable, Sendable { + enum Webview: Equatable, Sendable { + case primaryBtnClicked + case secondaryBtnClicked + case chooseSwitchToNonAdminUserClicked + case chooseCreateNonAdminClicked + case chooseDemoteAdminClicked + case connectChildSubmitted(Int) + } + + enum Delegate: Equatable, Sendable { + case saveCurrentStep(State.Step?) + } + + case webview(Webview) + case delegate(Delegate) + case resume(Resume) + case receivedDeviceData( + currentUserId: uid_t, + users: [MacOSUser], + notificationsSetting: NotificationsSetting + ) + case connectUser(TaskResult) + case setStep(State.Step) + } + + struct Reducer: FeatureReducer { + @Dependency(\.api) var api + @Dependency(\.app) var app + @Dependency(\.device) var device + @Dependency(\.filterExtension) var systemExtension + @Dependency(\.mainQueue) var mainQueue + @Dependency(\.monitoring) var monitoring + @Dependency(\.storage) var storage + + func reduce(into state: inout State, action: Action) -> Effect { + let step = state.step + let userIsAdmin = state.currentUser?.isAdmin != false + switch action { + + case .resume(.at(let step)): + state.windowOpen = true + state.step = step + return .none + + case .resume(.checkingScreenRecordingPermission): + return .exec { send in + await send(.setStep( + await monitoring.screenRecordingPermissionGranted() + ? .allowScreenshots_success + : .allowScreenshots_failed + )) + } + + case .receivedDeviceData(let currentUserId, let users, let notificationsSetting): + state.users = users.map(State.MacUser.init) + state.currentUser = state.users.first(where: { $0.id == currentUserId }) + state.existingNotificationsSetting = notificationsSetting + return .none + + case .webview(.primaryBtnClicked) where step == .welcome: + state.step = .confirmGertrudeAccount + return .exec { send in + await send(.receivedDeviceData( + currentUserId: device.currentUserId(), + users: try await device.listMacOSUsers(), + notificationsSetting: await device.notificationsSetting() + )) + } + + case .webview(.primaryBtnClicked) where step == .confirmGertrudeAccount: + state.step = .macosUserAccountType + return .none + + case .webview(.secondaryBtnClicked) where step == .confirmGertrudeAccount: + state.step = .noGertrudeAccount + return .none + + case .webview(.secondaryBtnClicked) where step == .noGertrudeAccount: + return .exec { _ in + await storage.deleteAll() + await app.quit() + } + + case .webview(.primaryBtnClicked) where step == .macosUserAccountType && !userIsAdmin: + state.step = .getChildConnectionCode + return .none + + // they choose to ignore the warning about user type and proceed + case .webview(.secondaryBtnClicked) where step == .macosUserAccountType && userIsAdmin: + state.step = .getChildConnectionCode + return .none + + // they click "show me how to fix" on the BAD mac os user landing page + case .webview(.primaryBtnClicked) where step == .macosUserAccountType && userIsAdmin: + state.userRemediationStep = state.users.count == 1 ? .create : .choose + return .send(.delegate(.saveCurrentStep(.macosUserAccountType))) + + case .webview(.chooseDemoteAdminClicked): + state.userRemediationStep = .demote + return .none + + case .webview(.chooseCreateNonAdminClicked): + state.userRemediationStep = .create + return .none + + case .webview(.chooseSwitchToNonAdminUserClicked): + state.userRemediationStep = .switch + return .none + + case .webview(.primaryBtnClicked) where step == .getChildConnectionCode: + state.step = .connectChild + return .none + + case .webview(.connectChildSubmitted(let code)): + state.connectChildRequest = .ongoing + return .exec { send in + await send(.connectUser((TaskResult { + try await api.connectUser(.init(code: code, device: device, app: app)) + }))) + } + + case .connectUser(.success(let user)): + state.connectChildRequest = .succeeded(payload: user.name) + return .none + + case .connectUser(.failure(let error)): + state.connectChildRequest = .failed(error: error.userMessage()) + return .none + + case .webview(.primaryBtnClicked) + where step == .connectChild && state.connectChildRequest.isFailed: + state.connectChildRequest = .idle + state.step = .getChildConnectionCode + return .none + + case .webview(.secondaryBtnClicked) + where step == .connectChild && state.connectChildRequest.isFailed: + return .exec { _ in + await device.openWebUrl(.contact) + } + + case .webview(.primaryBtnClicked) + where step == .connectChild && state.connectChildRequest.isSucceeded: + state.step = state.existingNotificationsSetting == .alert + ? .allowScreenshots_required + : .allowNotifications_start + return .none + + case .webview(.primaryBtnClicked) where step == .allowNotifications_start: + state.step = .allowNotifications_grant + return .exec { _ in + await device.requestNotificationAuthorization() + await device.openSystemPrefs(.notifications) + } + + case .webview(.primaryBtnClicked) + where step == .allowNotifications_grant || step == .allowNotifications_failed: + return .exec { send in + await send(.setStep( + await device.notificationsSetting() != .none + ? .allowScreenshots_required + : .allowNotifications_failed + )) + } + + case .webview(.secondaryBtnClicked) + where step == .allowNotifications_start || step == .allowNotifications_failed: + state.step = .allowScreenshots_required + return .none + + case .webview(.primaryBtnClicked) where step == .allowScreenshots_required: + return .exec { send in + await send(.setStep( + await monitoring.screenRecordingPermissionGranted() + ? .allowKeylogging_required + : .allowScreenshots_openSysSettings + )) + } + + case .webview(.secondaryBtnClicked) where step == .allowScreenshots_required: + state.step = .allowKeylogging_required + return .none + + case .webview(.primaryBtnClicked) where step == .allowScreenshots_openSysSettings: + state.step = .allowScreenshots_grantAndRestart + return .none + + case .webview(.primaryBtnClicked) where step == .allowScreenshots_failed: + state.step = .allowScreenshots_grantAndRestart + return .exec { _ in + await device.openSystemPrefs(.security(.screenRecording)) + } + + case .webview(.secondaryBtnClicked) where step == .allowScreenshots_failed: + state.step = .allowKeylogging_required + return .none + + case .webview(.primaryBtnClicked) where step == .allowScreenshots_success: + state.step = .allowKeylogging_required + return .none + + case .webview(.primaryBtnClicked) where step == .allowKeylogging_required: + return .exec { send in + await send(.setStep( + await monitoring.keystrokeRecordingPermissionGranted() + ? .installSysExt_explain + : .allowKeylogging_openSysSettings + )) + } + + case .webview(.secondaryBtnClicked) where step == .allowKeylogging_required: + state.step = .installSysExt_explain + return .none + + case .webview(.primaryBtnClicked) where step == .allowKeylogging_openSysSettings: + state.step = .allowKeylogging_grant + return .none + + case .webview(.primaryBtnClicked) where step == .allowKeylogging_grant: + return .exec { send in + await send(.setStep( + await monitoring.keystrokeRecordingPermissionGranted() + ? .installSysExt_explain + : .allowKeylogging_failed + )) + } + + case .webview(.primaryBtnClicked) where step == .allowKeylogging_failed: + state.step = .allowKeylogging_grant + return .exec { _ in + await device.openSystemPrefs(.security(.accessibility)) + } + + case .webview(.primaryBtnClicked) where step == .installSysExt_explain: + return .exec { send in + switch await systemExtension.state() { + case .notInstalled: + await send(.setStep(.installSysExt_allow)) + try? await mainQueue.sleep(for: .seconds(3)) // let them see the explanation gif + switch await systemExtension.install() { + case .installedSuccessfully: + await send(.setStep(.installSysExt_success)) + case .timedOutWaiting, .userClickedDontAllow: + await send(.setStep(.installSysExt_failed)) + case .alreadyInstalled: + // should never happen, since checked the condition above + await send(.setStep(.installSysExt_failed)) + case .activationRequestFailed, + .failedToGetBundleIdentifier, + .failedToLoadConfig, + .failedToSaveConfig: + await send(.setStep(.installSysExt_failed)) + } + case .errorLoadingConfig, .unknown: + await send(.setStep(.installSysExt_failed)) + case .installedAndRunning: + await send(.setStep(.installSysExt_success)) + case .installedButNotRunning: + if await systemExtension.start() == .installedAndRunning { + await send(.setStep(.installSysExt_success)) + } else { + // TODO: should we try to replace once? + await send(.setStep(.installSysExt_failed)) + } + } + } + + case .webview(.primaryBtnClicked) where step == .installSysExt_allow: + return .exec { send in + if await systemExtension.state() == .installedAndRunning { + await send(.setStep(.installSysExt_success)) + } else { + await send(.setStep(.installSysExt_failed)) + } + } + + case .webview(.secondaryBtnClicked) where step == .installSysExt_allow: + state.step = .installSysExt_failed + return .none + + case .webview(.primaryBtnClicked) where step == .installSysExt_failed: + state.step = .installSysExt_explain + return .none + + case .webview(.secondaryBtnClicked) where step == .installSysExt_failed: + state.step = .locateMenuBarIcon + return .none + + case .webview(.primaryBtnClicked) where step == .installSysExt_success: + state.step = .locateMenuBarIcon + return .none + + case .webview(.primaryBtnClicked) where step == .locateMenuBarIcon: + state.step = .viewHealthCheck + return .none + + case .webview(.primaryBtnClicked) where step == .viewHealthCheck: + state.step = .howToUseGertrude + return .none + + case .webview(.primaryBtnClicked) where step == .howToUseGertrude: + state.step = .finish + return .none + + case .webview(.primaryBtnClicked): + // TODO: debug assert, and error log + return .none + + case .webview(.secondaryBtnClicked): + // TODO: debug assert, and error log + return .none + + case .setStep(let step): + state.step = step + state.windowOpen = true // for resuming + return .none + + case .delegate: + return .none + } + } + } +} + +extension OnboardingFeature.State { + enum Step: Equatable, Codable { + case welcome + case confirmGertrudeAccount + case noGertrudeAccount + case macosUserAccountType + case getChildConnectionCode + case connectChild + case allowNotifications_start + case allowNotifications_grant + case allowNotifications_failed + case allowScreenshots_required + case allowScreenshots_openSysSettings + case allowScreenshots_grantAndRestart + + // these two states exist to give us a landing spot for resuming + // onboarding after the grant -> quit & reopen flow + case allowScreenshots_failed + case allowScreenshots_success + + case allowKeylogging_required + case allowKeylogging_openSysSettings + case allowKeylogging_grant + case allowKeylogging_failed + + case installSysExt_explain + case installSysExt_allow + case installSysExt_failed + case installSysExt_success + + case locateMenuBarIcon + case viewHealthCheck + case howToUseGertrude + case finish + } +} + +extension OnboardingFeature.State.MacUser { + init(_ user: MacOSUser) { + id = user.id + name = user.name + isAdmin = user.type == .admin + } +} diff --git a/macapp/App/Sources/App/PqlError+Message.swift b/macapp/App/Sources/App/PqlError+Message.swift index f859cd2b..f908dfd1 100644 --- a/macapp/App/Sources/App/PqlError+Message.swift +++ b/macapp/App/Sources/App/PqlError+Message.swift @@ -2,15 +2,17 @@ import PairQL extension Error { func userMessage(_ tags: [PqlError.AppTag: String] = [:], generic: String? = nil) -> String { + let fallback = + "Sorry, something went wrong. Please try again, or contact help if the problem persists." guard let pqlError = self as? PqlError else { - return generic ?? "Please try again, or contact help if the problem persists." + return generic ?? fallback } if let appTag = pqlError.appTag, let message = tags[appTag] { return message } else if let userMessage = pqlError.userMessage { return userMessage } else { - return generic ?? "Please try again, or contact help if the problem persists." + return generic ?? fallback } } } diff --git a/macapp/App/Sources/App/RequestState.swift b/macapp/App/Sources/App/RequestState.swift index 27a03509..d2fc062e 100644 --- a/macapp/App/Sources/App/RequestState.swift +++ b/macapp/App/Sources/App/RequestState.swift @@ -12,11 +12,30 @@ enum PayloadRequestState { case ongoing case succeeded(payload: T) case failed(error: E) + + var isSucceeded: Bool { + switch self { + case .succeeded: + return true + default: + return false + } + } + + var isFailed: Bool { + switch self { + case .failed: + return true + default: + return false + } + } } extension RequestState: Equatable where E: Equatable {} extension RequestState: Sendable where E: Sendable {} extension PayloadRequestState: Equatable where T: Equatable, E: Equatable {} +extension PayloadRequestState: Sendable where T: Sendable, E: Sendable {} extension RequestState: Codable where E: Codable { private struct _NamedCase: Codable { @@ -67,3 +86,59 @@ extension RequestState: Codable where E: Codable { } } } + +extension PayloadRequestState: Codable where T: Codable, E: Codable { + private struct _NamedCase: Codable { + var `case`: String + static func extract(from decoder: Decoder) throws -> String { + let container = try decoder.singleValueContainer() + return try container.decode(_NamedCase.self).case + } + } + + private struct _TypeScriptDecodeError: Error { + var message: String + } + + private struct _CaseSucceeded: Codable { + var `case` = "succeeded" + var payload: T + } + + private struct _CaseFailed: Codable { + var `case` = "failed" + var error: E + } + + func encode(to encoder: Encoder) throws { + switch self { + case .succeeded(let payload): + try _CaseSucceeded(payload: payload).encode(to: encoder) + case .failed(let error): + try _CaseFailed(error: error).encode(to: encoder) + case .idle: + try _NamedCase(case: "idle").encode(to: encoder) + case .ongoing: + try _NamedCase(case: "ongoing").encode(to: encoder) + } + } + + init(from decoder: Decoder) throws { + let caseName = try _NamedCase.extract(from: decoder) + let container = try decoder.singleValueContainer() + switch caseName { + case "succeeded": + let value = try container.decode(_CaseSucceeded.self) + self = .succeeded(payload: value.payload) + case "failed": + let value = try container.decode(_CaseFailed.self) + self = .failed(error: value.error) + case "idle": + self = .idle + case "ongoing": + self = .ongoing + default: + throw _TypeScriptDecodeError(message: "Unexpected case name: `\(caseName)`") + } + } +} diff --git a/macapp/App/Sources/App/Types.swift b/macapp/App/Sources/App/Types.swift index 1af06b7b..b12bc988 100644 --- a/macapp/App/Sources/App/Types.swift +++ b/macapp/App/Sources/App/Types.swift @@ -54,3 +54,7 @@ enum NotificationsSetting: String, Equatable, Codable { case banner case alert } + +extension URL { + static let contact = URL(string: "https://gertrude.app/contact")! +} diff --git a/macapp/App/Sources/ClientInterfaces/ApiClient.swift b/macapp/App/Sources/ClientInterfaces/ApiClient.swift index cabb54a7..28928dbe 100644 --- a/macapp/App/Sources/ClientInterfaces/ApiClient.swift +++ b/macapp/App/Sources/ClientInterfaces/ApiClient.swift @@ -84,6 +84,20 @@ public extension ApiClient { extension ApiClient: TestDependencyKey { public static let testValue = Self( + checkIn: unimplemented("ApiClient.checkIn"), + clearUserToken: unimplemented("ApiClient.clearUserToken"), + connectUser: unimplemented("ApiClient.connectUser"), + createKeystrokeLines: unimplemented("ApiClient.createKeystrokeLines"), + createSuspendFilterRequest: unimplemented("ApiClient.createSuspendFilterRequest"), + createUnlockRequests: unimplemented("ApiClient.createUnlockRequests"), + logInterestingEvent: unimplemented("ApiClient.logInterestingEvent"), + recentAppVersions: unimplemented("ApiClient.recentAppVersions"), + setAccountActive: unimplemented("ApiClient.setAccountActive"), + setUserToken: unimplemented("ApiClient.setUserToken"), + uploadScreenshot: unimplemented("ApiClient.uploadScreenshot") + ) + + public static let mock = Self( checkIn: { _ in throw Error.unexpectedError(statusCode: 999) }, clearUserToken: {}, connectUser: { _ in throw Error.unexpectedError(statusCode: 888) }, @@ -98,24 +112,6 @@ extension ApiClient: TestDependencyKey { ) } -#if DEBUG - public extension ApiClient { - static let failing = Self( - checkIn: unimplemented("ApiClient.checkIn"), - clearUserToken: unimplemented("ApiClient.clearUserToken"), - connectUser: unimplemented("ApiClient.connectUser"), - createKeystrokeLines: unimplemented("ApiClient.createKeystrokeLines"), - createSuspendFilterRequest: unimplemented("ApiClient.createSuspendFilterRequest"), - createUnlockRequests: unimplemented("ApiClient.createUnlockRequests"), - logInterestingEvent: unimplemented("ApiClient.logInterestingEvent"), - recentAppVersions: unimplemented("ApiClient.recentAppVersions"), - setAccountActive: unimplemented("ApiClient.setAccountActive"), - setUserToken: unimplemented("ApiClient.setUserToken"), - uploadScreenshot: unimplemented("ApiClient.uploadScreenshot") - ) - } -#endif - public extension DependencyValues { var api: ApiClient { get { self[ApiClient.self] } diff --git a/macapp/App/Sources/ClientInterfaces/AppClient.swift b/macapp/App/Sources/ClientInterfaces/AppClient.swift index 0b55a7b2..d9b7fa9b 100644 --- a/macapp/App/Sources/ClientInterfaces/AppClient.swift +++ b/macapp/App/Sources/ClientInterfaces/AppClient.swift @@ -37,6 +37,15 @@ public struct AppClient: Sendable { extension AppClient: TestDependencyKey { public static let testValue = Self( + colorScheme: unimplemented("AppClient.colorScheme"), + colorSchemeChanges: unimplemented("AppClient.colorSchemeChanges"), + disableLaunchAtLogin: unimplemented("AppClient.disableLaunchAtLogin"), + enableLaunchAtLogin: unimplemented("AppClient.enableLaunchAtLogin"), + isLaunchAtLoginEnabled: unimplemented("AppClient.isLaunchAtLoginEnabled"), + installedVersion: unimplemented("AppClient.installedVersion"), + quit: unimplemented("AppClient.quit") + ) + public static let mock = Self( colorScheme: { .light }, colorSchemeChanges: { Empty().eraseToAnyPublisher() }, disableLaunchAtLogin: {}, diff --git a/macapp/App/Sources/ClientInterfaces/FilterExtensionClient.swift b/macapp/App/Sources/ClientInterfaces/FilterExtensionClient.swift index 54ca4b16..fd6ce1d3 100644 --- a/macapp/App/Sources/ClientInterfaces/FilterExtensionClient.swift +++ b/macapp/App/Sources/ClientInterfaces/FilterExtensionClient.swift @@ -41,6 +41,19 @@ public struct FilterExtensionClient: Sendable { extension FilterExtensionClient: TestDependencyKey { public static let testValue = Self( + setup: unimplemented("FilterExtensionClient.setup"), + start: unimplemented("FilterExtensionClient.start"), + stop: unimplemented("FilterExtensionClient.stop"), + reinstall: unimplemented("FilterExtensionClient.reinstall"), + restart: unimplemented("FilterExtensionClient.restart"), + replace: unimplemented("FilterExtensionClient.replace"), + state: unimplemented("FilterExtensionClient.state"), + install: unimplemented("FilterExtensionClient.install"), + stateChanges: unimplemented("FilterExtensionClient.stateChanges"), + uninstall: unimplemented("FilterExtensionClient.uninstall") + ) + + public static let mock = Self( setup: { .installedAndRunning }, start: { .installedAndRunning }, stop: { .installedButNotRunning }, diff --git a/macapp/App/Sources/ClientInterfaces/FilterXPCClient.swift b/macapp/App/Sources/ClientInterfaces/FilterXPCClient.swift index c6a0f678..d2885ac0 100644 --- a/macapp/App/Sources/ClientInterfaces/FilterXPCClient.swift +++ b/macapp/App/Sources/ClientInterfaces/FilterXPCClient.swift @@ -64,6 +64,23 @@ public struct FilterXPCClient: Sendable { extension FilterXPCClient: TestDependencyKey { public static var testValue: Self { + .init( + establishConnection: unimplemented("FilterXPCClient.establishConnection"), + checkConnectionHealth: unimplemented("FilterXPCClient.checkConnectionHealth"), + disconnectUser: unimplemented("FilterXPCClient.disconnectUser"), + endFilterSuspension: unimplemented("FilterXPCClient.endFilterSuspension"), + requestAck: unimplemented("FilterXPCClient.requestAck"), + requestExemptUserIds: unimplemented("FilterXPCClient.requestExemptUserIds"), + sendDeleteAllStoredState: unimplemented("FilterXPCClient.sendDeleteAllStoredState"), + sendUserRules: unimplemented("FilterXPCClient.sendUserRules"), + setBlockStreaming: unimplemented("FilterXPCClient.setBlockStreaming"), + setUserExemption: unimplemented("FilterXPCClient.setUserExemption"), + suspendFilter: unimplemented("FilterXPCClient.suspendFilter"), + events: unimplemented("FilterXPCClient.events") + ) + } + + public static var mock: Self { .init( establishConnection: { .success(()) }, checkConnectionHealth: { .success(()) }, diff --git a/macapp/App/Sources/ClientInterfaces/WebSocketClient.swift b/macapp/App/Sources/ClientInterfaces/WebSocketClient.swift index 56eac9a7..6685dddc 100644 --- a/macapp/App/Sources/ClientInterfaces/WebSocketClient.swift +++ b/macapp/App/Sources/ClientInterfaces/WebSocketClient.swift @@ -46,6 +46,14 @@ extension WebSocketClient: EndpointOverridable { extension WebSocketClient: TestDependencyKey { public static let testValue = Self( + connect: unimplemented("WebSocketClient.connect"), + disconnect: unimplemented("WebSocketClient.disconnect"), + receive: unimplemented("WebSocketClient.receive"), + send: unimplemented("WebSocketClient.send"), + state: unimplemented("WebSocketClient.state") + ) + + public static let mock = Self( connect: { _ in .connected }, disconnect: {}, receive: { Empty().eraseToAnyPublisher() }, diff --git a/macapp/App/Sources/Core/UserDefaultsClient.swift b/macapp/App/Sources/Core/UserDefaultsClient.swift index aca97fb4..1c4a7851 100644 --- a/macapp/App/Sources/Core/UserDefaultsClient.swift +++ b/macapp/App/Sources/Core/UserDefaultsClient.swift @@ -44,6 +44,12 @@ extension UserDefaultsClient: DependencyKey { extension UserDefaultsClient: TestDependencyKey { public static let testValue = Self( + setString: unimplemented("UserDefaultsClient.setString"), + getString: unimplemented("UserDefaultsClient.getString"), + remove: unimplemented("UserDefaultsClient.remove"), + removeAll: unimplemented("UserDefaultsClient.removeAll") + ) + public static let mock = Self( setString: { _, _ in }, getString: { _ in nil }, remove: { _ in }, diff --git a/macapp/App/Sources/Filter/Dependencies/ExtensionClient.swift b/macapp/App/Sources/Filter/Dependencies/ExtensionClient.swift index 33913eeb..84d31d66 100644 --- a/macapp/App/Sources/Filter/Dependencies/ExtensionClient.swift +++ b/macapp/App/Sources/Filter/Dependencies/ExtensionClient.swift @@ -16,6 +16,9 @@ extension ExtensionClient: DependencyKey { extension ExtensionClient: TestDependencyKey { static let testValue = Self( + version: unimplemented("ExtensionClient.version") + ) + static let mock = Self( version: { "1.0.0" } ) } diff --git a/macapp/App/Sources/Filter/Dependencies/SecurityClient.swift b/macapp/App/Sources/Filter/Dependencies/SecurityClient.swift index d31c8ce9..790d0ebc 100644 --- a/macapp/App/Sources/Filter/Dependencies/SecurityClient.swift +++ b/macapp/App/Sources/Filter/Dependencies/SecurityClient.swift @@ -87,6 +87,10 @@ extension SecurityClient: DependencyKey { extension SecurityClient: TestDependencyKey { public static let testValue = Self( + userIdFromAuditToken: unimplemented("SecurityClient.userIdFromAuditToken"), + rootAppFromAuditToken: unimplemented("SecurityClient.rootAppFromAuditToken") + ) + public static let mock = Self( userIdFromAuditToken: { _ in nil }, rootAppFromAuditToken: { _ in (nil, nil) } ) diff --git a/macapp/App/Sources/Filter/Dependencies/StorageClient.swift b/macapp/App/Sources/Filter/Dependencies/StorageClient.swift index ac073ba2..c41a07e2 100644 --- a/macapp/App/Sources/Filter/Dependencies/StorageClient.swift +++ b/macapp/App/Sources/Filter/Dependencies/StorageClient.swift @@ -47,6 +47,13 @@ extension StorageClient: DependencyKey { extension StorageClient: TestDependencyKey { static let testValue = Self( + savePersistentState: unimplemented("StorageClient.savePersistentState"), + loadPersistentState: unimplemented("StorageClient.loadPersistentState"), + loadPersistentStateSync: unimplemented("StorageClient.loadPersistentStateSync"), + deleteAllPersistentState: unimplemented("StorageClient.deleteAllPersistentState"), + deleteAll: unimplemented("StorageClient.deleteAll") + ) + static let mock = Self( savePersistentState: { _ in }, loadPersistentState: { nil }, loadPersistentStateSync: { nil }, diff --git a/macapp/App/Sources/Filter/Dependencies/XPCClient.swift b/macapp/App/Sources/Filter/Dependencies/XPCClient.swift index 662534e1..c943ee80 100644 --- a/macapp/App/Sources/Filter/Dependencies/XPCClient.swift +++ b/macapp/App/Sources/Filter/Dependencies/XPCClient.swift @@ -38,6 +38,13 @@ extension XPCClient: DependencyKey { extension XPCClient: TestDependencyKey { static let testValue = Self( + notifyFilterSuspensionEnded: unimplemented("XPCClient.notifyFilterSuspensionEnded"), + startListener: unimplemented("XPCClient.startListener"), + stopListener: unimplemented("XPCClient.stopListener"), + sendBlockedRequest: unimplemented("XPCClient.sendBlockedRequest"), + events: unimplemented("XPCClient.events") + ) + static let mock = Self( notifyFilterSuspensionEnded: { _ in }, startListener: {}, stopListener: {}, diff --git a/macapp/App/Sources/LiveFilterExtensionClient/SystemClient.swift b/macapp/App/Sources/LiveFilterExtensionClient/SystemClient.swift index 6b9325b2..93e815f7 100644 --- a/macapp/App/Sources/LiveFilterExtensionClient/SystemClient.swift +++ b/macapp/App/Sources/LiveFilterExtensionClient/SystemClient.swift @@ -74,6 +74,19 @@ extension SystemClient: DependencyKey { extension SystemClient: TestDependencyKey { static let testValue = Self( + loadFilterConfiguration: unimplemented("SystemClient.loadFilterConfiguration"), + isNEFilterManagerSharedEnabled: unimplemented("SystemClient.isNEFilterManagerSharedEnabled"), + enableNEFilterManagerShared: unimplemented("SystemClient.enableNEFilterManagerShared"), + disableNEFilterManagerShared: unimplemented("SystemClient.disableNEFilterManagerShared"), + filterProviderConfiguration: unimplemented("SystemClient.filterProviderConfiguration"), + removeFilterConfiguration: unimplemented("SystemClient.removeFilterConfiguration"), + requestExtensionActivation: unimplemented("SystemClient.requestExtensionActivation"), + updateNEFilterManagerShared: unimplemented("SystemClient.updateNEFilterManagerShared"), + saveNEFilterManagerShared: unimplemented("SystemClient.saveNEFilterManagerShared"), + filterDidChangePublisher: unimplemented("SystemClient.filterDidChangePublisher") + ) + + static let mock = Self( loadFilterConfiguration: { .doesNotExistOrLoadedSuccessfully }, isNEFilterManagerSharedEnabled: { true }, enableNEFilterManagerShared: {}, diff --git a/macapp/App/Tests/AppTests/AdminWindowTests.swift b/macapp/App/Tests/AppTests/AdminWindowTests.swift index 332d61e5..33e227ee 100644 --- a/macapp/App/Tests/AppTests/AdminWindowTests.swift +++ b/macapp/App/Tests/AppTests/AdminWindowTests.swift @@ -38,7 +38,8 @@ import XExpect appVersion: "1.0.0", appUpdateReleaseChannel: .stable, filterVersion: "1.0.0", - user: nil + user: nil, + resumeOnboarding: nil )]) } diff --git a/macapp/App/Tests/AppTests/AppMigratorTests.swift b/macapp/App/Tests/AppTests/AppMigratorTests.swift index 63ef82fb..0deb379e 100644 --- a/macapp/App/Tests/AppTests/AppMigratorTests.swift +++ b/macapp/App/Tests/AppTests/AppMigratorTests.swift @@ -11,7 +11,7 @@ class AppMigratorTests: XCTestCase { typealias V1 = AppMigrator.Legacy.V1.StorageKey var testMigrator: AppMigrator { - AppMigrator(api: .failing, userDefaults: .failing) + AppMigrator(api: .testValue, userDefaults: .failing) } func testNoStoredDataAtAllReturnsNil() async { @@ -79,165 +79,174 @@ class AppMigratorTests: XCTestCase { } func testMigratesLegacyV1DataFromApiCallSuccess() async { - var migrator = testMigrator - - let apiUser = UserData( - id: .zeros, - token: .deadbeef, - deviceId: .twos, - name: "Big Mac", - keyloggingEnabled: true, - screenshotsEnabled: false, - screenshotFrequency: 6, - screenshotSize: 7, - connectedAt: Date(timeIntervalSince1970: 33) - ) - - let checkIn = spy( - on: CheckIn.Input.self, - returning: CheckIn.Output.mock { $0.userData = apiUser } - ) - migrator.api.checkIn = checkIn.fn - - let setApiToken = spy(on: UUID.self, returning: ()) - migrator.api.setUserToken = setApiToken.fn - - let setStringInvocations = LockIsolated<[Both]>([]) - migrator.userDefaults.setString = { key, value in - setStringInvocations.append(.init(key, value)) - } + await withDependencies { + $0.app.installedVersion = { "1.0.0" } + } operation: { + + var migrator = testMigrator + + let apiUser = UserData( + id: .zeros, + token: .deadbeef, + deviceId: .twos, + name: "Big Mac", + keyloggingEnabled: true, + screenshotsEnabled: false, + screenshotFrequency: 6, + screenshotSize: 7, + connectedAt: Date(timeIntervalSince1970: 33) + ) + + let checkIn = spy( + on: CheckIn.Input.self, + returning: CheckIn.Output.mock { $0.userData = apiUser } + ) + migrator.api.checkIn = checkIn.fn + + let setApiToken = spy(on: UUID.self, returning: ()) + migrator.api.setUserToken = setApiToken.fn + + let setStringInvocations = LockIsolated<[Both]>([]) + migrator.userDefaults.setString = { key, value in + setStringInvocations.append(.init(key, value)) + } - let getStringInvocations = LockIsolated<[String]>([]) - migrator.userDefaults.getString = { key in - getStringInvocations.append(key) - switch key { - case "persistent.state.v1": - return nil - case "persistent.state.v2": - return nil - case V1.userToken.namespaced: - return UUID.deadbeef.uuidString - case V1.installedAppVersion.namespaced: - return "1.77.88" - default: - XCTFail("Unexpected key: \(key)") - return nil + let getStringInvocations = LockIsolated<[String]>([]) + migrator.userDefaults.getString = { key in + getStringInvocations.append(key) + switch key { + case "persistent.state.v1": + return nil + case "persistent.state.v2": + return nil + case V1.userToken.namespaced: + return UUID.deadbeef.uuidString + case V1.installedAppVersion.namespaced: + return "1.77.88" + default: + XCTFail("Unexpected key: \(key)") + return nil + } } - } - let result = await migrator.migrate() - await expect(setApiToken.invocations).toEqual([.deadbeef]) - expect(await checkIn.invocations.value).toHaveCount(1) - expect(getStringInvocations.value).toEqual([ - "persistent.state.v2", - "persistent.state.v1", - V1.userToken.namespaced, - V1.installedAppVersion.namespaced, - ]) - expect(result).toEqual(.init( - appVersion: "1.77.88", - appUpdateReleaseChannel: .stable, - filterVersion: "1.77.88", - user: apiUser - )) - expect(setStringInvocations.value).toEqual([ - Both( + let result = await migrator.migrate() + await expect(setApiToken.invocations).toEqual([.deadbeef]) + expect(await checkIn.invocations.value).toHaveCount(1) + expect(getStringInvocations.value).toEqual([ "persistent.state.v2", - try! JSON.encode(Persistent.V2( - appVersion: "1.77.88", - appUpdateReleaseChannel: .stable, - filterVersion: "1.77.88", - user: apiUser - )) - ), - ]) + "persistent.state.v1", + V1.userToken.namespaced, + V1.installedAppVersion.namespaced, + ]) + expect(result).toEqual(.init( + appVersion: "1.77.88", + appUpdateReleaseChannel: .stable, + filterVersion: "1.77.88", + user: apiUser + )) + expect(setStringInvocations.value).toEqual([ + Both( + "persistent.state.v2", + try! JSON.encode(Persistent.V2( + appVersion: "1.77.88", + appUpdateReleaseChannel: .stable, + filterVersion: "1.77.88", + user: apiUser + )) + ), + ]) + } } func testMigratesLegacyV1DataWhenApiCallFails() async { - var migrator = testMigrator + await withDependencies { + $0.app.installedVersion = { "1.0.0" } + } operation: { + var migrator = testMigrator - // simulate that we can't fetch the user from the api - // so we need to pull all of the old info from storage - migrator.api.checkIn = { _ in throw TestErr("oh noes") } + // simulate that we can't fetch the user from the api + // so we need to pull all of the old info from storage + migrator.api.checkIn = { _ in throw TestErr("oh noes") } - let setApiToken = spy(on: UUID.self, returning: ()) - migrator.api.setUserToken = setApiToken.fn + let setApiToken = spy(on: UUID.self, returning: ()) + migrator.api.setUserToken = setApiToken.fn - let setStringInvocations = LockIsolated<[Both]>([]) - migrator.userDefaults.setString = { key, value in - setStringInvocations.append(.init(key, value)) - } + let setStringInvocations = LockIsolated<[Both]>([]) + migrator.userDefaults.setString = { key, value in + setStringInvocations.append(.init(key, value)) + } - let getStringInvocations = LockIsolated<[String]>([]) - migrator.userDefaults.getString = { key in - getStringInvocations.append(key) - switch key { - case "persistent.state.v1": - return nil - case "persistent.state.v2": - return nil - case V1.userToken.namespaced: - return UUID.ones.uuidString - case V1.installedAppVersion.namespaced: - return "1.77.88" - case V1.gertrudeUserId.namespaced: - return UUID.twos.uuidString - case V1.gertrudeDeviceId.namespaced: - return UUID.zeros.uuidString - case V1.keyloggingEnabled.namespaced: - return "true" - case V1.screenshotsEnabled.namespaced: - return "false" - case V1.screenshotFrequency.namespaced: - return "444" - case V1.screenshotSize.namespaced: - return "777" - default: - XCTFail("Unexpected key: \(key)") - return nil + let getStringInvocations = LockIsolated<[String]>([]) + migrator.userDefaults.getString = { key in + getStringInvocations.append(key) + switch key { + case "persistent.state.v1": + return nil + case "persistent.state.v2": + return nil + case V1.userToken.namespaced: + return UUID.ones.uuidString + case V1.installedAppVersion.namespaced: + return "1.77.88" + case V1.gertrudeUserId.namespaced: + return UUID.twos.uuidString + case V1.gertrudeDeviceId.namespaced: + return UUID.zeros.uuidString + case V1.keyloggingEnabled.namespaced: + return "true" + case V1.screenshotsEnabled.namespaced: + return "false" + case V1.screenshotFrequency.namespaced: + return "444" + case V1.screenshotSize.namespaced: + return "777" + default: + XCTFail("Unexpected key: \(key)") + return nil + } } - } - let result = await migrator.migrate() - let expectedUser = UserData( - id: .twos, - token: .ones, - deviceId: .zeros, - name: "(unknown)", - keyloggingEnabled: true, - screenshotsEnabled: false, - screenshotFrequency: 444, - screenshotSize: 777, - connectedAt: Date(timeIntervalSince1970: 0) - ) - - await expect(setApiToken.invocations).toEqual([.ones]) - expect(getStringInvocations.value).toEqual([ - "persistent.state.v2", - "persistent.state.v1", - V1.userToken.namespaced, - V1.installedAppVersion.namespaced, - V1.gertrudeUserId.namespaced, - V1.gertrudeDeviceId.namespaced, - V1.keyloggingEnabled.namespaced, - V1.screenshotsEnabled.namespaced, - V1.screenshotFrequency.namespaced, - V1.screenshotSize.namespaced, - ]) - expect(result).toEqual(.init( - appVersion: "1.77.88", - appUpdateReleaseChannel: .stable, - filterVersion: "1.77.88", - user: expectedUser - )) - expect(setStringInvocations.value).toEqual([Both( - "persistent.state.v2", - try! JSON.encode(Persistent.V2( + let result = await migrator.migrate() + let expectedUser = UserData( + id: .twos, + token: .ones, + deviceId: .zeros, + name: "(unknown)", + keyloggingEnabled: true, + screenshotsEnabled: false, + screenshotFrequency: 444, + screenshotSize: 777, + connectedAt: Date(timeIntervalSince1970: 0) + ) + + await expect(setApiToken.invocations).toEqual([.ones]) + expect(getStringInvocations.value).toEqual([ + "persistent.state.v2", + "persistent.state.v1", + V1.userToken.namespaced, + V1.installedAppVersion.namespaced, + V1.gertrudeUserId.namespaced, + V1.gertrudeDeviceId.namespaced, + V1.keyloggingEnabled.namespaced, + V1.screenshotsEnabled.namespaced, + V1.screenshotFrequency.namespaced, + V1.screenshotSize.namespaced, + ]) + expect(result).toEqual(.init( appVersion: "1.77.88", appUpdateReleaseChannel: .stable, filterVersion: "1.77.88", user: expectedUser )) - )]) + expect(setStringInvocations.value).toEqual([Both( + "persistent.state.v2", + try! JSON.encode(Persistent.V2( + appVersion: "1.77.88", + appUpdateReleaseChannel: .stable, + filterVersion: "1.77.88", + user: expectedUser + )) + )]) + } } } diff --git a/macapp/App/Tests/AppTests/AppReducerTests.swift b/macapp/App/Tests/AppTests/AppReducerTests.swift index bb8701de..630596fe 100644 --- a/macapp/App/Tests/AppTests/AppReducerTests.swift +++ b/macapp/App/Tests/AppTests/AppReducerTests.swift @@ -11,7 +11,7 @@ import XExpect @MainActor final class AppReducerTests: XCTestCase { func testDidFinishLaunching_Exhaustive() async { - let (store, bgQueue) = AppReducer.testStore(exhaustive: true) + let (store, _) = AppReducer.testStore(exhaustive: true) let filterSetupSpy = ActorIsolated(false) store.deps.filterExtension.setup = { @@ -33,20 +33,20 @@ import XExpect $0.history.userConnection = .established(welcomeDismissed: true) } - await store.receive(.websocket(.connectedSuccessfully)) - - await expect(tokenSetSpy).toEqual(UserData.mock.token) - - await bgQueue.advance(by: .milliseconds(5)) await expect(filterSetupSpy).toEqual(true) await store.receive(.filter(.receivedState(.installedButNotRunning))) { $0.filter.extension = .installedButNotRunning } + await store.receive(.websocket(.connectedSuccessfully)) + + await expect(tokenSetSpy).toEqual(UserData.mock.token) + + await store.receive(.startHeartbeat) + let prevUser = store.state.user.data - await bgQueue.advance(by: .milliseconds(5)) await store.receive(.checkIn(result: .success(.mock), reason: .appLaunched)) { $0.appUpdates.latestVersion = .init(semver: "2.0.4") $0.user.data?.screenshotsEnabled = true @@ -71,6 +71,16 @@ import XExpect await store.send(.application(.willTerminate)) // cancel heartbeat } + func testOnboardingDelegateSaveStepPersists() async { + let (store, _) = AppReducer.testStore() + let saveState = spy(on: Persistent.State.self, returning: ()) + store.deps.storage.savePersistentState = saveState.fn + + await store.send(.onboarding(.delegate(.saveCurrentStep(.macosUserAccountType)))) + await expect(saveState.invocations.value[0].resumeOnboarding) + .toEqual(.at(step: .macosUserAccountType)) + } + func testDidFinishLaunching_EstablishesConnectionIfFilterOn() async { let (store, bgQueue) = AppReducer.testStore() store.deps.storage.loadPersistentState = { nil } @@ -127,20 +137,31 @@ import XExpect extension AppReducer { static func testStore>( exhaustive: Bool = false, + mockDeps: Bool = true, reducer: R = AppReducer(), mutateState: @escaping (inout State) -> Void = { _ in } ) -> (TestStoreOf, TestSchedulerOf) { - var state = State() + var state = State(appVersion: "1.0.0") mutateState(&state) let store = TestStore(initialState: state, reducer: { reducer }) + store.useMainSerialExecutor = true store.exhaustivity = exhaustive ? .on : .off let scheduler = DispatchQueue.test - store.deps.date = .constant(Date(timeIntervalSince1970: 0)) - store.deps.backgroundQueue = scheduler.eraseToAnyScheduler() - store.deps.mainQueue = .immediate - store.deps.storage.loadPersistentState = { .mock } - store.deps.api.checkIn = { _ in .mock } - store.deps.filterExtension.setup = { .installedAndRunning } + if mockDeps { + store.deps.date = .constant(Date(timeIntervalSince1970: 0)) + store.deps.backgroundQueue = scheduler.eraseToAnyScheduler() + store.deps.mainQueue = .immediate + store.deps.monitoring = .mock + store.deps.storage = .mock + store.deps.storage.loadPersistentState = { .mock } + store.deps.app = .mock + store.deps.api = .mock + store.deps.device = .mock + store.deps.api.checkIn = { _ in .mock } + store.deps.filterExtension = .mock + store.deps.filterXpc = .mock + store.deps.websocket = .mock + } return (store, scheduler) } } diff --git a/macapp/App/Tests/AppTests/BlockedRequestsFeatureTests.swift b/macapp/App/Tests/AppTests/BlockedRequestsFeatureTests.swift index d3033a6a..1d7d9bef 100644 --- a/macapp/App/Tests/AppTests/BlockedRequestsFeatureTests.swift +++ b/macapp/App/Tests/AppTests/BlockedRequestsFeatureTests.swift @@ -200,8 +200,9 @@ import XExpect } await store.receive(.createUnlockRequests(.failure(TestErr("")))) { - $0.createUnlockRequests = - .failed(error: "Please try again, or contact help if the problem persists.") + $0.createUnlockRequests = .failed( + error: "Sorry, something went wrong. Please try again, or contact help if the problem persists." + ) } // toggling a request brings back to idle so the user can try again diff --git a/macapp/App/Tests/AppTests/FilterFeatureTests.swift b/macapp/App/Tests/AppTests/FilterFeatureTests.swift index 9964e09d..3acbc0d2 100644 --- a/macapp/App/Tests/AppTests/FilterFeatureTests.swift +++ b/macapp/App/Tests/AppTests/FilterFeatureTests.swift @@ -57,7 +57,11 @@ import XExpect } func testManualAdminSuspensionLifecycle() async { - let store = TestStore(initialState: AppReducer.State()) { AppReducer() } + let store = TestStore(initialState: AppReducer.State(appVersion: "1.0.0")) { + AppReducer() + } + store.deps.websocket = .mock + store.deps.device = .mock store.deps.date = .constant(Date(timeIntervalSince1970: 0)) let suspendFilter = spy(on: Seconds.self, returning: Result.success(())) store.deps.filterXpc.suspendFilter = suspendFilter.fn diff --git a/macapp/App/Tests/AppTests/HistoryUserConnectionTests.swift b/macapp/App/Tests/AppTests/HistoryUserConnectionTests.swift index 8bd16499..cbff0fbd 100644 --- a/macapp/App/Tests/AppTests/HistoryUserConnectionTests.swift +++ b/macapp/App/Tests/AppTests/HistoryUserConnectionTests.swift @@ -57,7 +57,9 @@ import XExpect await store.receive(.history(.userConnection(.connect(.failure(TestErr("Oh no!")))))) { $0.history.userConnection = - .connectFailed("Please try again, or contact help if the problem persists.") + .connectFailed( + "Sorry, something went wrong. Please try again, or contact help if the problem persists." + ) } await store.send(.menuBar(.connectFailedHelpClicked)) diff --git a/macapp/App/Tests/AppTests/MonitoringFeatureTests.swift b/macapp/App/Tests/AppTests/MonitoringFeatureTests.swift index 107074fe..34540e03 100644 --- a/macapp/App/Tests/AppTests/MonitoringFeatureTests.swift +++ b/macapp/App/Tests/AppTests/MonitoringFeatureTests.swift @@ -617,8 +617,14 @@ import XExpect func testConnectingUserStartsMonitoring() async { let (store, bgQueue) = AppReducer.testStore() - store.deps.storage.loadPersistentState = { nil } store.deps.api.checkIn = { _ in throw TestErr("stop launch checkin") } + store.deps.storage.loadPersistentState = { .init( + appVersion: "1.0.0", + appUpdateReleaseChannel: .stable, + filterVersion: "1.0.0", + user: nil, // <-- no user + resumeOnboarding: nil + ) } let (takeScreenshot, uploadScreenshot, _) = spyScreenshots(store) let keylogging = spyKeylogging(store, keystrokes: mock( diff --git a/macapp/App/Tests/AppTests/OnboardingFeatureTests.swift b/macapp/App/Tests/AppTests/OnboardingFeatureTests.swift new file mode 100644 index 00000000..c50b086e --- /dev/null +++ b/macapp/App/Tests/AppTests/OnboardingFeatureTests.swift @@ -0,0 +1,691 @@ +import Combine +import ComposableArchitecture +import Core +import MacAppRoute +import TestSupport +import XCTest +import XExpect + +@testable import App + +@MainActor final class OnboardingFeatureTests: XCTestCase { + func testFirstBootOnboardingHappyPathExhaustive() async { + let (store, _) = AppReducer.testStore(exhaustive: true, mockDeps: false) + store.deps.mainQueue = .immediate + + // TODO: this is a little weird that i have to mock these, seems like + // maybe some listeners shouldn't initialize until we start the heartbeat? or something? + store.deps.filterExtension.stateChanges = { Empty().eraseToAnyPublisher() } + store.deps.filterXpc.events = { Empty().eraseToAnyPublisher() } + store.deps.websocket.receive = { Empty().eraseToAnyPublisher() } + store.deps.websocket.state = { .notConnected } + + store.deps.device.currentUserId = { 502 } + store.deps.device.listMacOSUsers = DeviceClient.mock.listMacOSUsers + store.deps.device.notificationsSetting = { .none } + + store.deps.storage.loadPersistentState = { nil } // <-- first boot + let saveState = spy(on: Persistent.State.self, returning: ()) + store.deps.storage.savePersistentState = saveState.fn + let extSetup = mock(always: FilterExtensionState.notInstalled) + store.deps.filterExtension.setup = extSetup.fn + + await store.send(.application(.didFinishLaunching)) + + await store.receive(.loadedPersistentState(nil)) { + $0.onboarding.windowOpen = true + $0.onboarding.step = .welcome + } + + await store.receive(.filter(.receivedState(.notInstalled))) { + $0.filter.extension = .notInstalled + } + + await expect(extSetup.invocations).toEqual(1) + await expect(saveState.invocations.value).toHaveCount(1) + + // they click next on the welcome screen... + await store.send(.onboarding(.webview(.primaryBtnClicked))) { + $0.onboarding.step = .confirmGertrudeAccount // ... and go to confirm account + } + + await store.receive(.onboarding(.receivedDeviceData( + currentUserId: 502, + users: [ + .init(id: 501, name: "Dad", type: .admin), + .init(id: 502, name: "liljimmy", type: .standard), + ], + notificationsSetting: .none + ))) { + $0.onboarding.users = [ + .init(id: 501, name: "Dad", isAdmin: true), + .init(id: 502, name: "liljimmy", isAdmin: false), + ] + $0.onboarding.currentUser = .init(id: 502, name: "liljimmy", isAdmin: false) + $0.onboarding.existingNotificationsSetting = .some(.none) + } + + // next they confirm that they have a gertrude account... + store.deps.device.currentMacOsUserType = { .standard } + await store.send(.onboarding(.webview(.primaryBtnClicked))) { + $0.onboarding.step = .macosUserAccountType // ...and end up on the macos user screen + } + + // they click next on the macos user confirmation good page... + await store.send(.onboarding(.webview(.primaryBtnClicked))) { + $0.onboarding.step = .getChildConnectionCode // ...and go to the get connection screen + } + + // they click the "got it" button on get connection code screen... + await store.send(.onboarding(.webview(.primaryBtnClicked))) { + $0.onboarding.step = .connectChild // ... and end up on the connect child screen + $0.onboarding.connectChildRequest = .idle + } + + let user = UserData.mock { $0.name = "lil suzy" } + let connectUser = spy(on: ConnectUser.Input.self, returning: user) + store.deps.api.connectUser = connectUser.fn + store.deps.app.installedVersion = { "1.0.0" } + await expect(saveState.invocations.value).toHaveCount(1) + store.deps.device = .mock // lots of data used by connect user request + // they enter code `123456` and click submit... + await store.send(.onboarding(.webview(.connectChildSubmitted(123_456)))) { + $0.onboarding.step = .connectChild + $0.onboarding.connectChildRequest = .ongoing // ... and see a throbber + } + + await expect(connectUser.invocations.value).toHaveCount(1) + await expect(connectUser.invocations.value[0].verificationCode).toEqual(123_456) + + await store.receive(.onboarding(.connectUser(.success(user)))) { + $0.user.data = user + $0.onboarding.step = .connectChild + $0.onboarding.connectChildRequest = .succeeded(payload: "lil suzy") + } + + // we persisted the user data + await expect(saveState.invocations.value).toHaveCount(2) + await expect(saveState.invocations.value[1].user).toEqual(user) + + // they click "next" on the connected child success screen... + await store.send(.onboarding(.webview(.primaryBtnClicked))) { + $0.onboarding.step = .allowNotifications_start // ...and go to notifications screen + } + + let requestNotifAuth = mock(always: ()) + store.deps.device.requestNotificationAuthorization = requestNotifAuth.fn + let openSysPrefs = spy(on: SystemPrefsLocation.self, returning: ()) + store.deps.device.openSystemPrefs = openSysPrefs.fn + + // they click "Open System Settings" on the notifications start screen + await store.send(.onboarding(.webview(.primaryBtnClicked))) { + $0.onboarding.step = .allowNotifications_grant // ...and go to grant + } + + // ... and we requested authorization and then opened system prefs + await expect(requestNotifAuth.invocations).toEqual(1) + await expect(openSysPrefs.invocations).toEqual([.notifications]) + + // they did indeed enable notifications... + let notifsSettings = mock(always: NotificationsSetting.alert) + store.deps.device.notificationsSetting = notifsSettings.fn + // ... and then clicked "Done" on the notifications grant screen + await store.send(.onboarding(.webview(.primaryBtnClicked))) + + // ...and we confirmed the setting and moved them on the happy path + await expect(notifsSettings.invocations).toEqual(1) + await store.receive(.onboarding(.setStep(.allowScreenshots_required))) { + $0.onboarding.step = .allowScreenshots_required + } + + // they have not previously granted permission... + let screenshotsAllowed = mock(returning: [false], then: true) + store.deps.monitoring.screenRecordingPermissionGranted = screenshotsAllowed.fn + + // they click "Grant Permission" on the allow screenshots start screen + await store.send(.onboarding(.webview(.primaryBtnClicked))) + + // ...and we check the setting (which pops up prompt) and moved them on + await expect(screenshotsAllowed.invocations).toEqual(1) + await store.receive(.onboarding(.setStep(.allowScreenshots_openSysSettings))) { + $0.onboarding.step = .allowScreenshots_openSysSettings // ...and go to open + } + + // they click "Done" indicating that they clicked the system prompt + await store.send(.onboarding(.webview(.primaryBtnClicked))) { + $0.onboarding.step = .allowScreenshots_grantAndRestart // ...and go to grant + } + + // NB: here technically they RESTART the app, but instead of starting a new test + // we simulate receiving the resume action to carry on where they should + // we have other tests testing the resume from persisted state flow. + await store.send(.onboarding(.resume(.checkingScreenRecordingPermission))) + + await store.receive(.onboarding(.setStep(.allowScreenshots_success))) { + $0.onboarding.step = .allowScreenshots_success + } + + // they click the "Next" button from the screen recording success + await store.send(.onboarding(.webview(.primaryBtnClicked))) { + $0.onboarding.step = .allowKeylogging_required // ...and go to keylogging + } + + // they have not previously granted permission... + let keyloggingAllowed = mock(returning: [false], then: true) + store.deps.monitoring.keystrokeRecordingPermissionGranted = keyloggingAllowed.fn + + // they click "Grant Permission" on the allow keylogging start screen + await store.send(.onboarding(.webview(.primaryBtnClicked))) + + // ...and we check the setting (which pops up prompt) and moved them on + await expect(keyloggingAllowed.invocations).toEqual(1) + await store.receive(.onboarding(.setStep(.allowKeylogging_openSysSettings))) { + $0.onboarding.step = .allowKeylogging_openSysSettings // ...and go to open + } + + // they click "Done" indicating that they clicked the system prompt + await store.send(.onboarding(.webview(.primaryBtnClicked))) { + $0.onboarding.step = .allowKeylogging_grant // ...and go to grant + } + + // they click "Done" indicating they think they've allowed keylogging + await store.send(.onboarding(.webview(.primaryBtnClicked))) + + // we confirm, and see that the did it correct... + await expect(keyloggingAllowed.invocations).toEqual(2) + // ...so they get sent off to the next happy path step + await store.receive(.onboarding(.setStep(.installSysExt_explain))) { + $0.onboarding.step = .installSysExt_explain // ...and go to sys ext start + } + + let filterState = mock(returning: [FilterExtensionState.notInstalled, .installedAndRunning]) + store.deps.filterExtension.state = filterState.fn + let installSysExt = mock(once: FilterInstallResult.installedSuccessfully) + store.deps.filterExtension.install = installSysExt.fn + + // they click "Next" on the install sys ext start screen + await store.send(.onboarding(.webview(.primaryBtnClicked))) + await expect(installSysExt.invocations).toEqual(1) + await store.receive(.onboarding(.setStep(.installSysExt_allow))) { + $0.onboarding.step = .installSysExt_allow // ...and go to sys ext allow + } + + // becuase filterExtension.install is mocked to return success, we go to success + await store.receive(.onboarding(.setStep(.installSysExt_success))) { + $0.onboarding.step = .installSysExt_success + } + + // they click "Next" on the install sys ext success screen + await store.send(.onboarding(.webview(.primaryBtnClicked))) { + $0.onboarding.step = .locateMenuBarIcon // ...and go to locate icon + } + + // they click "Next" on the locate menu bar icon screen + await store.send(.onboarding(.webview(.primaryBtnClicked))) { + $0.onboarding.step = .viewHealthCheck // ...and go to health check + } + + // they click "Next" on the health check screen + await store.send(.onboarding(.webview(.primaryBtnClicked))) { + $0.onboarding.step = .howToUseGertrude // ...and go to how to use + } + + // they click "Next" on the how to use screen + await store.send(.onboarding(.webview(.primaryBtnClicked))) { + $0.onboarding.step = .finish // ...and go to finish + } + } + + func testClickingTryAgainPrimaryFromInstallSysExtFailed() async { + let store = featureStore { $0.step = .installSysExt_failed } + await store.send(.webview(.primaryBtnClicked)) { + $0.step = .installSysExt_explain + } + } + + func testClickingSkipSecondaryFromInstallSysExtFailed() async { + let store = featureStore { $0.step = .installSysExt_failed } + await store.send(.webview(.secondaryBtnClicked)) { + $0.step = .locateMenuBarIcon + } + } + + func testClickingHelpSecondaryFromInstallSysExt() async { + let store = featureStore { $0.step = .installSysExt_allow } + await store.send(.webview(.secondaryBtnClicked)) { + $0.step = .installSysExt_failed + } + } + + // for most users, we will move them along automatically to + // success of failure based on the result of the install request, + // but we do have a button as well, this tests that it works + func testClickingDoneFromInstallSysExt() async { + let store = featureStore { $0.step = .installSysExt_allow } + let filterState = mock(returning: [FilterExtensionState.installedAndRunning]) + store.deps.filterExtension.state = filterState.fn + + await store.send(.webview(.primaryBtnClicked)) + await store.receive(.setStep(.installSysExt_success)) + await expect(filterState.invocations).toEqual(1) + } + + func testHandleDetectingSysExtInstallFail() async { + let store = featureStore { + $0.step = .installSysExt_explain + } + store.deps.mainQueue = .immediate + let filterState = mock(once: FilterExtensionState.notInstalled) + store.deps.filterExtension.state = filterState.fn + let installSysExt = mock(once: FilterInstallResult.timedOutWaiting) // <-- fail + store.deps.filterExtension.install = installSysExt.fn + + // they click "Next" on the install sys ext explain screen + await store.send(.webview(.primaryBtnClicked)) + await expect(installSysExt.invocations).toEqual(1) + await store.receive(.setStep(.installSysExt_allow)) { + $0.step = .installSysExt_allow // ...and go to sys ext allow + } + + await store.receive(.setStep(.installSysExt_failed)) + } + + func testSysExtAlreadyInstalledAndRunning() async { + let store = featureStore { + $0.step = .installSysExt_explain + } + let filterState = mock(once: FilterExtensionState.installedAndRunning) + store.deps.filterExtension.state = filterState.fn + + await store.send(.webview(.primaryBtnClicked)) + await store.receive(.setStep(.installSysExt_success)) { + $0.step = .installSysExt_success + } + } + + func testSysExtAlreadyInstalledButNotRunning_StartsToSuccess() async { + let store = featureStore { $0.step = .installSysExt_explain } + let filterState = mock(once: FilterExtensionState.installedButNotRunning) + store.deps.filterExtension.state = filterState.fn + let filterStart = mock(once: FilterExtensionState.installedAndRunning) + store.deps.filterExtension.start = filterStart.fn + + await store.send(.webview(.primaryBtnClicked)) + await store.receive(.setStep(.installSysExt_success)) { + $0.step = .installSysExt_success + } + + await expect(filterStart.invocations).toEqual(1) + } + + func testSysExtAlreadyInstalledButNotRunning_StartFailsToError() async { + let store = featureStore { $0.step = .installSysExt_explain } + let filterState = mock(once: FilterExtensionState.installedButNotRunning) + store.deps.filterExtension.state = filterState.fn + let filterStart = mock(once: FilterExtensionState.installedButNotRunning) + store.deps.filterExtension.start = filterStart.fn + + await store.send(.webview(.primaryBtnClicked)) + await store.receive(.setStep(.installSysExt_failed)) { + $0.step = .installSysExt_failed + } + } + + func testSkipAllowKeylogging() async { + let store = featureStore { $0.step = .allowKeylogging_required } + await store.send(.webview(.secondaryBtnClicked)) { + $0.step = .installSysExt_explain + } + } + + func testFailedToAllowKeylogging() async { + let store = featureStore { $0.step = .allowKeylogging_grant } + let keyloggingAllowed = mock(always: false) // <-- they failed to allow + store.deps.monitoring.keystrokeRecordingPermissionGranted = keyloggingAllowed.fn + + await store.send(.webview(.primaryBtnClicked)) + + // ...and we check the setting and move to failure + await expect(keyloggingAllowed.invocations).toEqual(1) + await store.receive(.setStep(.allowKeylogging_failed)) { + $0.step = .allowKeylogging_failed + } + + let openSysPrefs = spy(on: SystemPrefsLocation.self, returning: ()) + store.deps.device.openSystemPrefs = openSysPrefs.fn + + // now they click the "try again" button + await store.send(.webview(.primaryBtnClicked)) { + $0.step = .allowKeylogging_grant + } + + // and we tried to open system prefs to the right spot + await expect(openSysPrefs.invocations).toEqual([.security(.accessibility)]) + } + + func testSkipsMostKeyloggingStepsIfPermsPreviouslyGranted() async { + let store = featureStore { $0.step = .allowKeylogging_required } + + let keyloggingAllowed = mock(always: true) // <- they have granted permission + store.deps.monitoring.keystrokeRecordingPermissionGranted = keyloggingAllowed.fn + + // they click "Grant permission" on the allow screenshots required screen + await store.send(.webview(.primaryBtnClicked)) + + // ...and we check the setting (which pops up prompt) and moved them on + await expect(keyloggingAllowed.invocations).toEqual(1) + await store.receive(.setStep(.installSysExt_explain)) { + $0.step = .installSysExt_explain // ...and go to install system extension + } + } + + func testSkipAllowingScreenshots() async { + let store = featureStore { $0.step = .allowScreenshots_required } + // they click "Skip" on the allow screenshots start screen + await store.send(.webview(.secondaryBtnClicked)) { + $0.step = .allowKeylogging_required // ...and go to keylogging + } + } + + func testSkipsMostScreenshotStepsIfPermsPreviouslyGranted() async { + let store = featureStore { $0.step = .allowScreenshots_required } + + let screenshotsAllowed = mock(always: true) // <- they have granted permission + store.deps.monitoring.screenRecordingPermissionGranted = screenshotsAllowed.fn + + // they click "Grant permission" on the allow screenshots required screen + await store.send(.webview(.primaryBtnClicked)) + + // ...and we check the setting (which pops up prompt) and moved them on + await expect(screenshotsAllowed.invocations).toEqual(1) + await store.receive(.setStep(.allowKeylogging_required)) { + $0.step = .allowKeylogging_required // ...and go to keylogging + } + } + + func testFailureToGrantNotificationsSendsToFailScreen() async { + let store = featureStore { + $0.step = .allowNotifications_grant + } + + let notifsSettings = mock( + returning: [NotificationsSetting.none], // <- they did NOT enable notifications... + then: NotificationsSetting.alert // ... but they fix it before we check again + ) + store.deps.device.notificationsSetting = notifsSettings.fn + + // ... and then clicked "Done" on the notifications grant screen + await store.send(.webview(.primaryBtnClicked)) + + // ...and we fail to confirm the setting, moving them to fail screen + await expect(notifsSettings.invocations).toEqual(1) + await store.receive(.setStep(.allowNotifications_failed)) { + $0.step = .allowNotifications_failed + } + + // they fixed it, and clicked Try Again... + await store.send(.webview(.primaryBtnClicked)) + + // ...and we confirmed the setting and moved them on the happy path + await expect(notifsSettings.invocations).toEqual(2) + await store.receive(.setStep(.allowScreenshots_required)) { + $0.step = .allowScreenshots_required + } + } + + func testSkipFromAllowNotificationsFailedStep() async { + let store = featureStore { + $0.step = .allowNotifications_failed + } + await store.send(.webview(.secondaryBtnClicked)) { + $0.step = .allowScreenshots_required + } + } + + func testSkipAllowNotificationsStep() async { + let store = featureStore { + $0.step = .allowNotifications_start + } + await store.send(.webview(.secondaryBtnClicked)) { + $0.step = .allowScreenshots_required + } + } + + func testNotificationsStepSkippedIfAlreadyGranted() async { + let store = featureStore { + $0.step = .connectChild + $0.connectChildRequest = .succeeded(payload: "lil suzy") + $0.existingNotificationsSetting = .alert // <-- already granted + } + + // from the connect child success, they click next... + await store.send(.webview(.primaryBtnClicked)) { + $0.step = .allowScreenshots_required // ...and skip straight to screenshots + } + } + + func testConnectChildFailure() async { + let (store, _) = AppReducer.testStore { + $0.onboarding.step = .connectChild + $0.onboarding.connectChildRequest = .ongoing + } + let openWebUrl = spy(on: URL.self, returning: ()) + store.deps.device.openWebUrl = openWebUrl.fn + + await store.send(.onboarding(.connectUser(.failure(TestErr("oh noes!"))))) { + $0.onboarding.step = .connectChild + $0.onboarding.connectChildRequest = .failed( + error: "Sorry, something went wrong. Please try again, or contact help if the problem persists." + ) + } + + // they clicked the "get help" button + await store.send(.onboarding(.webview(.secondaryBtnClicked))) + await expect(openWebUrl.invocations).toEqual([.init(string: "https://gertrude.app/contact")!]) + + // they clicked "try again" + await store.send(.onboarding(.webview(.primaryBtnClicked))) { + $0.onboarding.step = .getChildConnectionCode + $0.onboarding.connectChildRequest = .idle + } + } + + func testResumingAtMacOSUserType() async { + let (store, _) = AppReducer.testStore() + let saveState = spy(on: Persistent.State.self, returning: ()) + store.deps.storage.savePersistentState = saveState.fn + store.deps.storage.loadPersistentState = { .init( + appVersion: "1.0.0", + appUpdateReleaseChannel: .stable, + filterVersion: "1.0.0", + user: nil, + resumeOnboarding: .at(step: .macosUserAccountType) + ) } + + await store.send(.application(.didFinishLaunching)) + + await store.receive(.onboarding(.resume(.at(step: .macosUserAccountType)))) { + $0.onboarding.windowOpen = true + $0.onboarding.step = .macosUserAccountType + } + + await expect(saveState.invocations).toEqual([ + .init( + appVersion: "1.0.0", + appUpdateReleaseChannel: .stable, + filterVersion: "1.0.0", + user: nil, + resumeOnboarding: nil // <-- nils out step + ), + ]) + } + + func testResumingCheckScreenRecordingGranted() async { + let (store, _) = AppReducer.testStore() + store.deps.monitoring.screenRecordingPermissionGranted = { true } // <-- granted + store.deps.storage.loadPersistentState = { .init( + appVersion: "1.0.0", + appUpdateReleaseChannel: .stable, + filterVersion: "1.0.0", + user: nil, + resumeOnboarding: .checkingScreenRecordingPermission // <-- check + ) } + + await store.send(.application(.didFinishLaunching)) + + await store.receive(.onboarding(.setStep(.allowScreenshots_success))) { + $0.onboarding.windowOpen = true + $0.onboarding.step = .allowScreenshots_success + } + } + + func testResumingCheckScreenRecordingNotGranted() async { + let (store, _) = AppReducer.testStore() + store.deps.monitoring.screenRecordingPermissionGranted = { false } // <-- NOT granted + store.deps.storage.loadPersistentState = { .init( + appVersion: "1.0.0", + appUpdateReleaseChannel: .stable, + filterVersion: "1.0.0", + user: nil, + resumeOnboarding: .checkingScreenRecordingPermission // <-- check + ) } + + await store.send(.application(.didFinishLaunching)) + + await store.receive(.onboarding(.setStep(.allowScreenshots_failed))) { + $0.onboarding.windowOpen = true + $0.onboarding.step = .allowScreenshots_failed + } + + let openSysPrefs = spy(on: SystemPrefsLocation.self, returning: ()) + store.deps.device.openSystemPrefs = openSysPrefs.fn + + // they now click the primary "try again" button + await store.send(.onboarding(.webview(.primaryBtnClicked))) { + $0.onboarding.step = .allowScreenshots_grantAndRestart // ... and go back to the grant step + } + + // and we tried to open system prefs to the right spot + await expect(openSysPrefs.invocations).toEqual([.security(.screenRecording)]) + } + + func testSkipFromScreenRecordingFailed() async { + let store = featureStore { + $0.step = .allowScreenshots_failed + } + await store.send(.webview(.secondaryBtnClicked)) { + $0.step = .allowKeylogging_required + } + } + + func testNoGertrudeAccountQuit() async { + let store = featureStore() + store.deps.device = .mock + let quit = mock(once: ()) + store.deps.app.quit = quit.fn + let deleteAll = mock(once: ()) + store.deps.storage.deleteAll = deleteAll.fn + + await store.send(.webview(.primaryBtnClicked)) + + await store.send(.webview(.secondaryBtnClicked)) { + $0.step = .noGertrudeAccount + } + + await store.send(.webview(.secondaryBtnClicked)) + await expect(deleteAll.invoked).toEqual(true) + await expect(quit.invoked).toEqual(true) + } + + func testBadUserTypeIgnoresDanger() async { + let store = featureStore() + store.deps.device.currentUserId = { 501 } + store.deps.device.listMacOSUsers = { [.init(id: 501, name: "Dad", type: .admin)] } + store.deps.device.notificationsSetting = { .none } + + await store.send(.webview(.primaryBtnClicked)) { + $0.step = .confirmGertrudeAccount + } + + await store.send(.webview(.primaryBtnClicked)) { + $0.step = .macosUserAccountType + $0.userRemediationStep = nil + } + + await store.send(.webview(.secondaryBtnClicked)) { + $0.step = .getChildConnectionCode + } + } + + func testBadUserTypeNoChoice() async { + let store = featureStore() + store.deps.device.currentUserId = { 501 } + store.deps.device.listMacOSUsers = { [.init(id: 501, name: "Dad", type: .admin)] } + store.deps.device.notificationsSetting = { .none } + + await store.send(.webview(.primaryBtnClicked)) { + $0.step = .confirmGertrudeAccount + } + + // they click confirming they have a gertrude acct... + await store.send(.webview(.primaryBtnClicked)) { + $0.step = .macosUserAccountType // ...landing them on user type warning page + $0.userRemediationStep = nil + } + + // they click the primary btn: show me how to fix it... + await store.send(.webview(.primaryBtnClicked)) { + // because they only have ONE admin user on the system, + // we take them straight to step to create a new user + $0.userRemediationStep = .create + } + } + + func testBadUserTypeWithChoice() async { + let store = featureStore() + + store.deps.device.currentUserId = { 501 } + store.deps.device.listMacOSUsers = { [ + .init(id: 501, name: "Dad", type: .admin), + .init(id: 503, name: "Mom", type: .admin), + .init(id: 502, name: "liljimmy", type: .standard), + ] } + store.deps.device.notificationsSetting = { .none } + + await store.send(.webview(.primaryBtnClicked)) { + $0.step = .confirmGertrudeAccount + } + + // they click confirming they have a gertrude acct... + await store.send(.webview(.primaryBtnClicked)) { + $0.step = .macosUserAccountType // ...landing them on user type warning page + $0.userRemediationStep = nil + } + + // they click the primary btn: show me how to fix it... + await store.send(.webview(.primaryBtnClicked)) { + // because they have options for remediation, they must choose + $0.userRemediationStep = .choose + } + + // remediations require restarting gertrude, so note the step to restart w/ + await store.receive(.delegate(.saveCurrentStep(.macosUserAccountType))) + + await store.send(.webview(.chooseDemoteAdminClicked)) { + $0.userRemediationStep = .demote + } + } + + // helpers + func featureStore( + mutateState: @escaping (inout OnboardingFeature.State) -> Void = { _ in } + ) -> TestStoreOf { + var state = OnboardingFeature.State() + mutateState(&state) + let store = TestStore(initialState: state) { + OnboardingFeature.Reducer() + } + store.exhaustivity = .off + return store + } +} diff --git a/macapp/App/Tests/AppTests/UserFeatureTests.swift b/macapp/App/Tests/AppTests/UserFeatureTests.swift index 9a4cf220..b78127cc 100644 --- a/macapp/App/Tests/AppTests/UserFeatureTests.swift +++ b/macapp/App/Tests/AppTests/UserFeatureTests.swift @@ -41,9 +41,8 @@ import XExpect } func testSuccessfulApiRequestsRestartsCount() async { - let (store, _) = AppReducer.testStore { - $0.user = .init(data: .mock) - } + let (store, _) = AppReducer.testStore { $0.user = .init(data: .mock) } + store.deps.updater = .mock let error = PqlError( id: "123", diff --git a/macapp/App/Tests/FilterTests/FilterReducerTests.swift b/macapp/App/Tests/FilterTests/FilterReducerTests.swift index 4f3c24fd..23bf2c13 100644 --- a/macapp/App/Tests/FilterTests/FilterReducerTests.swift +++ b/macapp/App/Tests/FilterTests/FilterReducerTests.swift @@ -12,7 +12,9 @@ import XExpect func testExtensionStarted_Exhaustive() async { let (store, mainQueue) = Filter.testStore(exhaustive: true) let subject = PassthroughSubject() + store.deps.filterExtension = .mock store.deps.xpc.events = { subject.eraseToAnyPublisher() } + store.deps.xpc.stopListener = {} let startListener = mock(always: ()) store.deps.xpc.startListener = startListener.fn store.deps.storage.loadPersistentState = { .init( @@ -73,6 +75,7 @@ import XExpect func testStreamBlockedRequests() async { let (store, _) = Filter.testStore(exhaustive: true) + store.deps.filterExtension = .mock // user not streaming, so we won't send the request store.deps.xpc.sendBlockedRequest = { _, _ in fatalError() } @@ -114,6 +117,7 @@ import XExpect let (store, _) = Filter.testStore() let sendBlocked = spy2(on: (uid_t.self, BlockedRequest.self), returning: ()) store.deps.xpc.sendBlockedRequest = sendBlocked.fn + store.deps.filterExtension = .mock await store.send(.xpc(.receivedAppMessage(.setBlockStreaming( enabled: true, @@ -159,6 +163,7 @@ import XExpect let save = spy(on: Persistent.State.self, returning: ()) store.deps.storage.savePersistentState = save.fn + store.deps.filterExtension = .mock await store.send(.xpc(.receivedAppMessage(.disconnectUser(userId: 502)))) { $0.userKeys = [503: [key2]] @@ -174,9 +179,8 @@ import XExpect } func testSetUserExemption() async { - let (store, _) = Filter.testStore { - $0.exemptUsers = [501] - } + let (store, _) = Filter.testStore { $0.exemptUsers = [501] } + store.deps.filterExtension = .mock let save = spy(on: Persistent.State.self, returning: ()) store.deps.storage.savePersistentState = save.fn @@ -200,6 +204,7 @@ import XExpect let (store, mainQueue) = Filter.testStore { $0.suspensions = [503: otherUserSuspension] } + store.deps.filterExtension = .mock let notifyExpired = spy(on: uid_t.self, returning: ()) store.deps.xpc.notifyFilterSuspensionEnded = notifyExpired.fn @@ -230,6 +235,7 @@ import XExpect let notifyExpired = spy(on: uid_t.self, returning: ()) store.deps.xpc.notifyFilterSuspensionEnded = notifyExpired.fn + store.deps.filterExtension = .mock await store.send(.xpc(.receivedAppMessage(.suspendFilter(userId: 502, duration: 600)))) { $0.suspensions = [ @@ -251,6 +257,7 @@ import XExpect let notifyExpired = spy(on: uid_t.self, returning: ()) store.deps.xpc.notifyFilterSuspensionEnded = notifyExpired.fn + store.deps.filterExtension = .mock await store.send(.xpc(.receivedAppMessage(.suspendFilter(userId: 502, duration: 600)))) { $0.suspensions = [ @@ -308,6 +315,10 @@ import XExpect func testHeartbeatCleansUpDanglingSuspensionFromSleepConfusingTimer() async { let (store, mainQueue) = Filter.testStore() + store.deps.filterExtension = .mock + store.deps.xpc.events = XPCClient.mock.events + store.deps.xpc.startListener = {} + store.deps.storage.loadPersistentState = StorageClient.mock.loadPersistentState let time = ControllingNow(starting: .epoch, with: mainQueue) store.deps.date = time.generator From d36f0f863d91e51edd503e8e76dac3a84107aa9a Mon Sep 17 00:00:00 2001 From: Jared Henderson Date: Mon, 2 Oct 2023 15:09:31 -0400 Subject: [PATCH 02/25] macapp: heartbeat and websocket connection contingent on user connection --- macapp/App/Sources/App/AppReducer.swift | 57 ++++++++++++------- .../App/Sources/App/AppUpdatesFeature.swift | 2 +- .../App/Sources/App/ApplicationFeature.swift | 36 +++--------- macapp/App/Sources/App/CheckInFeature.swift | 13 ++++- macapp/App/Sources/App/HistoryFeature.swift | 3 +- macapp/App/Sources/App/Types.swift | 16 ++---- .../Sources/App/UserConnectionFeature.swift | 6 +- .../App/Tests/AppTests/AppReducerTests.swift | 5 +- .../AppTests/HistoryUserConnectionTests.swift | 15 +++-- .../AppTests/MonitoringFeatureTests.swift | 10 +--- .../AppTests/OnboardingFeatureTests.swift | 33 +++++++++-- 11 files changed, 107 insertions(+), 89 deletions(-) diff --git a/macapp/App/Sources/App/AppReducer.swift b/macapp/App/Sources/App/AppReducer.swift index 91871cfa..46fc2d1a 100644 --- a/macapp/App/Sources/App/AppReducer.swift +++ b/macapp/App/Sources/App/AppReducer.swift @@ -27,6 +27,11 @@ struct AppReducer: Reducer, Sendable { } } + enum CancelId { + case heartbeatInterval + case websocketMessages + } + enum Action: Equatable, Sendable { enum Delegate: Equatable, Sendable { case filterSuspendedChanged(was: Bool, is: Bool) @@ -37,6 +42,11 @@ struct AppReducer: Reducer, Sendable { case text(String, String) } + enum StartUserProtectionSource: Equatable, Sendable { + case persistence + case newConnection + } + case admin(AdminFeature.Action) case adminWindow(AdminWindowFeature.Action) case application(ApplicationFeature.Action) @@ -52,10 +62,10 @@ struct AppReducer: Reducer, Sendable { case onboarding(OnboardingFeature.Action) case loadedPersistentState(Persistent.State?) case user(UserFeature.Action) - case heartbeat(Heartbeat.Interval) + case heartbeat(HeartbeatInterval) case blockedRequests(BlockedRequestsFeature.Action) case requestSuspension(RequestSuspensionFeature.Action) - case startHeartbeat + case startProtecting(user: UserData, from: StartUserProtectionSource) case websocket(WebSocketFeature.Action) indirect case adminAuthed(Action) @@ -64,8 +74,10 @@ struct AppReducer: Reducer, Sendable { @Dependency(\.api) var api @Dependency(\.device) var device @Dependency(\.backgroundQueue) var bgQueue + @Dependency(\.mainQueue) var mainQueue @Dependency(\.network) var network @Dependency(\.storage) var storage + @Dependency(\.websocket) var websocket var body: some ReducerOf { Reduce { state, action in @@ -94,34 +106,39 @@ struct AppReducer: Reducer, Sendable { state.appUpdates.releaseChannel = persisted.appUpdateReleaseChannel state.filter.version = persisted.filterVersion guard let user = persisted.user else { - // TODO: are we sure we want to start the heartbeat? - return .exec { send in await send(.startHeartbeat) } + return .none } state.user = .init(data: user) + return .exec { send in + await send(.startProtecting(user: user, from: .persistence)) + } + + case .startProtecting(let user, let source): return .merge( - .exec { send in - await send(.startHeartbeat) - }, .exec { [filterVersion = state.filter.version] send in await api.setUserToken(user.token) guard network.isConnected() else { return } await send(.checkIn( result: TaskResult { try await api.appCheckIn(filterVersion) }, - reason: .appLaunched + reason: .init(source) )) - } - ) - - case .startHeartbeat: - return .exec { send in - var numTicks = 0 - for await _ in bgQueue.timer(interval: .seconds(60)) { - numTicks += 1 - for interval in heartbeatIntervals(for: numTicks) { - await send(.heartbeat(interval)) + }, + // todo, launch at login + // .publisher { + // websocket.receive() + // .map { .websocket(.receivedMessage($0)) } + // .receive(on: mainQueue) + // }.cancellable(id: CancelId.websocketMessages), + .exec { send in + var numTicks = 0 + for await _ in bgQueue.timer(interval: .seconds(60)) { + numTicks += 1 + for interval in heartbeatIntervals(for: numTicks) { + await send(.heartbeat(interval)) + } } - } - }.cancellable(id: Heartbeat.CancelId.interval) + }.cancellable(id: CancelId.heartbeatInterval) + ) case .focusedNotification(let notification): // dismiss windows/dropdowns so notification is visible, i.e. "focused" diff --git a/macapp/App/Sources/App/AppUpdatesFeature.swift b/macapp/App/Sources/App/AppUpdatesFeature.swift index f22480c7..18bb75bd 100644 --- a/macapp/App/Sources/App/AppUpdatesFeature.swift +++ b/macapp/App/Sources/App/AppUpdatesFeature.swift @@ -79,7 +79,7 @@ extension AppUpdatesFeature.RootReducer: FilterControlling { // refresh the rules post-update, or else health check will complain await send(.checkIn( result: TaskResult { try await api.appCheckIn(version) }, - reason: .appLaunched + reason: .appUpdated )) // big sur doesn't get notification pushed when filter restarts diff --git a/macapp/App/Sources/App/ApplicationFeature.swift b/macapp/App/Sources/App/ApplicationFeature.swift index 63e56235..04bdca01 100644 --- a/macapp/App/Sources/App/ApplicationFeature.swift +++ b/macapp/App/Sources/App/ApplicationFeature.swift @@ -15,11 +15,10 @@ enum ApplicationFeature { @Dependency(\.app) var app @Dependency(\.backgroundQueue) var bgQueue @Dependency(\.device) var device - @Dependency(\.mainQueue) var mainQueue @Dependency(\.storage) var storage @Dependency(\.filterXpc) var filterXpc @Dependency(\.filterExtension) var filterExtension - @Dependency(\.websocket) var websocket + @Dependency(\.mainQueue) var mainQueue } } @@ -29,19 +28,11 @@ extension ApplicationFeature.RootReducer: RootReducing { case .application(.didFinishLaunching): return .merge( - // .exec { _ in - // // requesting notification authorization at least once - // // ensures that the system prefs panel will show Gertrude - // // TODO: consider delaying this if no user connected - // await device.requestNotificationAuthorization() - // }, - .exec { send in await send(.loadedPersistentState(try await storage.loadPersistentState())) }, .exec { send in - // try await bgQueue.sleep(for: .milliseconds(5)) // <- unit test determinism let setupState = await filterExtension.setup() await send(.filter(.receivedState(setupState))) if setupState.installed { @@ -49,16 +40,6 @@ extension ApplicationFeature.RootReducer: RootReducing { } }, - // .exec { send in - // var numTicks = 0 - // for await _ in bgQueue.timer(interval: .seconds(60)) { - // numTicks += 1 - // for interval in heartbeatIntervals(for: numTicks) { - // await send(.heartbeat(interval)) - // } - // } - // }.cancellable(id: Heartbeat.CancelId.interval), - .exec { _ in // TODO: should be part of onboarding... // if await app.isLaunchAtLoginEnabled() == false { @@ -76,17 +57,14 @@ extension ApplicationFeature.RootReducer: RootReducing { filterXpc.events() .map { .xpc($0) } .receive(on: mainQueue) - }, - - .publisher { - websocket.receive() - .map { .websocket(.receivedMessage($0)) } - .receive(on: mainQueue) } ) case .application(.willTerminate): - return .cancel(id: Heartbeat.CancelId.interval) + return .merge( + .cancel(id: AppReducer.CancelId.heartbeatInterval), + .cancel(id: AppReducer.CancelId.websocketMessages) + ) default: return .none @@ -94,8 +72,8 @@ extension ApplicationFeature.RootReducer: RootReducing { } } -func heartbeatIntervals(for tick: Int) -> [Heartbeat.Interval] { - var intervals: [Heartbeat.Interval] = [.everyMinute] +func heartbeatIntervals(for tick: Int) -> [HeartbeatInterval] { + var intervals: [HeartbeatInterval] = [.everyMinute] if tick % 5 == 0 { intervals.append(.everyFiveMinutes) } diff --git a/macapp/App/Sources/App/CheckInFeature.swift b/macapp/App/Sources/App/CheckInFeature.swift index 783fce9b..3cbf68ed 100644 --- a/macapp/App/Sources/App/CheckInFeature.swift +++ b/macapp/App/Sources/App/CheckInFeature.swift @@ -129,11 +129,22 @@ extension CheckInFeature.RootReducer { extension CheckIn { enum Reason: Equatable, Sendable { - case appLaunched + case appUpdated case healthCheck case heartbeat + case loadedPersistedUser + case userConnected case inactiveAccountRechecked case receivedWebsocketMessage case userRefreshedRules + + init(_ source: AppReducer.Action.StartUserProtectionSource) { + switch source { + case .persistence: + self = .loadedPersistedUser + case .newConnection: + self = .userConnected + } + } } } diff --git a/macapp/App/Sources/App/HistoryFeature.swift b/macapp/App/Sources/App/HistoryFeature.swift index 025e3587..d53d821f 100644 --- a/macapp/App/Sources/App/HistoryFeature.swift +++ b/macapp/App/Sources/App/HistoryFeature.swift @@ -76,7 +76,8 @@ extension HistoryFeature.RootReducer: RootReducing { case .history(.userConnection(.connect(.success(let user)))), .onboarding(.connectUser(.success(let user))): state.user = .init(data: user) - return .exec { [persistent = state.persistent] _ in + return .exec { [persistent = state.persistent] send in + await send(.startProtecting(user: user, from: .newConnection)) try await storage.savePersistentState(persistent) } diff --git a/macapp/App/Sources/App/Types.swift b/macapp/App/Sources/App/Types.swift index b12bc988..4cad1fee 100644 --- a/macapp/App/Sources/App/Types.swift +++ b/macapp/App/Sources/App/Types.swift @@ -37,16 +37,12 @@ extension AdminAuthenticating where Action == AppReducer.Action { } } -enum Heartbeat { - enum Interval: Equatable, Sendable { - case everyMinute - case everyFiveMinutes - case everyTwentyMinutes - case everyHour - case everySixHours - } - - enum CancelId { case interval } +enum HeartbeatInterval: Equatable, Sendable { + case everyMinute + case everyFiveMinutes + case everyTwentyMinutes + case everyHour + case everySixHours } enum NotificationsSetting: String, Equatable, Codable { diff --git a/macapp/App/Sources/App/UserConnectionFeature.swift b/macapp/App/Sources/App/UserConnectionFeature.swift index ab347ab2..a23599f0 100644 --- a/macapp/App/Sources/App/UserConnectionFeature.swift +++ b/macapp/App/Sources/App/UserConnectionFeature.swift @@ -30,11 +30,9 @@ enum UserConnectionFeature: Feature { extension UserConnectionFeature.Reducer { func reduce(into state: inout State, action: Action) -> Effect { switch action { - case .connect(.success(let user)): + case .connect(.success): state = .established(welcomeDismissed: false) - return .exec { _ in - await api.setUserToken(user.token) - } + return .none case .connect(.failure(let error)): let codeNotFound = "Code not found, or expired. Try reentering, or create a new code." diff --git a/macapp/App/Tests/AppTests/AppReducerTests.swift b/macapp/App/Tests/AppTests/AppReducerTests.swift index 630596fe..b16478b0 100644 --- a/macapp/App/Tests/AppTests/AppReducerTests.swift +++ b/macapp/App/Tests/AppTests/AppReducerTests.swift @@ -39,15 +39,14 @@ import XExpect $0.filter.extension = .installedButNotRunning } + await store.receive(.startProtecting(user: .mock, from: .persistence)) await store.receive(.websocket(.connectedSuccessfully)) await expect(tokenSetSpy).toEqual(UserData.mock.token) - await store.receive(.startHeartbeat) - let prevUser = store.state.user.data - await store.receive(.checkIn(result: .success(.mock), reason: .appLaunched)) { + await store.receive(.checkIn(result: .success(.mock), reason: .loadedPersistedUser)) { $0.appUpdates.latestVersion = .init(semver: "2.0.4") $0.user.data?.screenshotsEnabled = true $0.user.data?.keyloggingEnabled = true diff --git a/macapp/App/Tests/AppTests/HistoryUserConnectionTests.swift b/macapp/App/Tests/AppTests/HistoryUserConnectionTests.swift index cbff0fbd..190e17d4 100644 --- a/macapp/App/Tests/AppTests/HistoryUserConnectionTests.swift +++ b/macapp/App/Tests/AppTests/HistoryUserConnectionTests.swift @@ -10,13 +10,12 @@ import XExpect let (store, _) = AppReducer.testStore() store.dependencies.api.connectUser = { _ in .mock } - let savedState = LockIsolated(nil) - store.dependencies.storage.savePersistentState = { state in - savedState.setValue(state) - } + store.deps.api.checkIn = { _ in throw TestErr("stop check in") } + let saveState = spy(on: Persistent.State.self, returning: ()) + store.deps.storage.savePersistentState = saveState.fn - let apiUserToken = ActorIsolated(nil) - store.deps.api.setUserToken = { await apiUserToken.setValue($0) } + let setUserToken = spy(on: UUID.self, returning: ()) + store.deps.api.setUserToken = setUserToken.fn await store.send(.menuBar(.connectClicked)) { $0.history.userConnection = .enteringConnectionCode @@ -33,8 +32,8 @@ import XExpect await store.receive(.websocket(.connectedSuccessfully)) - expect(savedState).toEqual(.mock) - await expect(apiUserToken).toEqual(UserData.mock.token) + await expect(saveState.invocations).toEqual([.mock]) + await expect(setUserToken.invocations).toEqual([UserData.mock.token]) await store.send(.menuBar(.welcomeAdminClicked)) { $0.history.userConnection = .established(welcomeDismissed: true) diff --git a/macapp/App/Tests/AppTests/MonitoringFeatureTests.swift b/macapp/App/Tests/AppTests/MonitoringFeatureTests.swift index 34540e03..9fa03803 100644 --- a/macapp/App/Tests/AppTests/MonitoringFeatureTests.swift +++ b/macapp/App/Tests/AppTests/MonitoringFeatureTests.swift @@ -515,8 +515,8 @@ import XExpect } } await store.send(.application(.didFinishLaunching)) - await bgQueue.advance(by: .seconds(600)) // <-- no fatal error - await expect(keylogging.take.invoked).toEqual(true) // we always check + await bgQueue.advance(by: .seconds(600)) // <-- no fatal error, heartbeat not running + await expect(keylogging.take.invoked).toEqual(false) await expect(keylogging.upload.invoked).toEqual(false) } @@ -627,13 +627,9 @@ import XExpect ) } let (takeScreenshot, uploadScreenshot, _) = spyScreenshots(store) - let keylogging = spyKeylogging(store, keystrokes: mock( - returning: [nil], - then: [.mock] - )) + let keylogging = spyKeylogging(store) await store.send(.application(.didFinishLaunching)) - await bgQueue.advance(by: .seconds(60 * 5)) // <- to heartbeat await expect(takeScreenshot.invocations.value.count).toEqual(0) await expect(uploadScreenshot.invocations.value.count).toEqual(0) await expect(keylogging.upload.invocations.value.count).toEqual(0) diff --git a/macapp/App/Tests/AppTests/OnboardingFeatureTests.swift b/macapp/App/Tests/AppTests/OnboardingFeatureTests.swift index c50b086e..513a30eb 100644 --- a/macapp/App/Tests/AppTests/OnboardingFeatureTests.swift +++ b/macapp/App/Tests/AppTests/OnboardingFeatureTests.swift @@ -1,6 +1,7 @@ import Combine import ComposableArchitecture import Core +import Gertie import MacAppRoute import TestSupport import XCTest @@ -11,13 +12,11 @@ import XExpect @MainActor final class OnboardingFeatureTests: XCTestCase { func testFirstBootOnboardingHappyPathExhaustive() async { let (store, _) = AppReducer.testStore(exhaustive: true, mockDeps: false) + let scheduler = DispatchQueue.test + store.deps.backgroundQueue = scheduler.eraseToAnyScheduler() store.deps.mainQueue = .immediate - - // TODO: this is a little weird that i have to mock these, seems like - // maybe some listeners shouldn't initialize until we start the heartbeat? or something? store.deps.filterExtension.stateChanges = { Empty().eraseToAnyPublisher() } store.deps.filterXpc.events = { Empty().eraseToAnyPublisher() } - store.deps.websocket.receive = { Empty().eraseToAnyPublisher() } store.deps.websocket.state = { .notConnected } store.deps.device.currentUserId = { 502 } @@ -82,12 +81,22 @@ import XExpect $0.onboarding.connectChildRequest = .idle } + // lots happens when the user connection is made... let user = UserData.mock { $0.name = "lil suzy" } let connectUser = spy(on: ConnectUser.Input.self, returning: user) store.deps.api.connectUser = connectUser.fn + let setUserToken = spy(on: UUID.self, returning: ()) + store.deps.api.setUserToken = setUserToken.fn + let setAccountActive = spy(on: Bool.self, returning: ()) + store.deps.api.setAccountActive = setAccountActive.fn + let checkInResult = CheckIn.Output.empty { $0.userData = user } + let checkIn = spy(on: CheckIn.Input.self, returning: checkInResult) + store.deps.api.checkIn = checkIn.fn store.deps.app.installedVersion = { "1.0.0" } await expect(saveState.invocations.value).toHaveCount(1) store.deps.device = .mock // lots of data used by connect user request + store.deps.websocket.receive = { Empty().eraseToAnyPublisher() } + // they enter code `123456` and click submit... await store.send(.onboarding(.webview(.connectChildSubmitted(123_456)))) { $0.onboarding.step = .connectChild @@ -96,6 +105,9 @@ import XExpect await expect(connectUser.invocations.value).toHaveCount(1) await expect(connectUser.invocations.value[0].verificationCode).toEqual(123_456) + await expect(setUserToken.invocations).toEqual([UserData.mock.token]) + await expect(setAccountActive.invocations).toEqual([true]) + await expect(checkIn.invocations).toEqual([.init(appVersion: "1.0.0", filterVersion: "1.0.0")]) await store.receive(.onboarding(.connectUser(.success(user)))) { $0.user.data = user @@ -103,8 +115,14 @@ import XExpect $0.onboarding.connectChildRequest = .succeeded(payload: "lil suzy") } + await store.receive(.startProtecting(user: user, from: .newConnection)) + await store.receive(.checkIn(result: .success(checkInResult), reason: .userConnected)) { + $0.appUpdates.latestVersion = checkInResult.latestRelease + } + await store.receive(.user(.updated(previous: user))) + // we persisted the user data - await expect(saveState.invocations.value).toHaveCount(2) + await expect(saveState.invocations.value).toHaveCount(3) await expect(saveState.invocations.value[1].user).toEqual(user) // they click "next" on the connected child success screen... @@ -234,6 +252,11 @@ import XExpect await store.send(.onboarding(.webview(.primaryBtnClicked))) { $0.onboarding.step = .finish // ...and go to finish } + + // shutdown tries fo flush keystrokes + store.deps.monitoring = .mock + store.deps.monitoring.takePendingKeystrokes = { nil } + await store.send(.application(.willTerminate)) } func testClickingTryAgainPrimaryFromInstallSysExtFailed() async { From 62b6a704ea4f94fdd2e33f58b7fd5fe3ed0a195e Mon Sep 17 00:00:00 2001 From: Jared Henderson Date: Mon, 2 Oct 2023 16:41:43 -0400 Subject: [PATCH 03/25] macapp: disconnecting user cancels heartbeat, websocket, and launch at login --- macapp/App/Sources/App/AppReducer.swift | 22 +++-- .../App/Sources/App/ApplicationFeature.swift | 7 -- macapp/App/Sources/App/CheckInFeature.swift | 2 +- macapp/App/Sources/App/HistoryFeature.swift | 12 ++- .../App/Sources/App/MonitoringFeature.swift | 4 +- .../App/Onboarding/OnboardingFeature.swift | 10 ++ .../Sources/App/UserConnectionFeature.swift | 18 ++-- macapp/App/Sources/App/WebSocketFeature.swift | 2 + .../App/Tests/AppTests/AppReducerTests.swift | 21 ++-- .../AppTests/OnboardingFeatureTests.swift | 11 ++- .../Tests/AppTests/UserConnectionTests.swift | 99 +++++++++++++++++++ 11 files changed, 171 insertions(+), 37 deletions(-) create mode 100644 macapp/App/Tests/AppTests/UserConnectionTests.swift diff --git a/macapp/App/Sources/App/AppReducer.swift b/macapp/App/Sources/App/AppReducer.swift index 46fc2d1a..ddecc69e 100644 --- a/macapp/App/Sources/App/AppReducer.swift +++ b/macapp/App/Sources/App/AppReducer.swift @@ -44,7 +44,8 @@ struct AppReducer: Reducer, Sendable { enum StartUserProtectionSource: Equatable, Sendable { case persistence - case newConnection + case onboardingConnection + case menuBarConnection } case admin(AdminFeature.Action) @@ -72,6 +73,7 @@ struct AppReducer: Reducer, Sendable { } @Dependency(\.api) var api + @Dependency(\.app) var app @Dependency(\.device) var device @Dependency(\.backgroundQueue) var bgQueue @Dependency(\.mainQueue) var mainQueue @@ -123,12 +125,18 @@ struct AppReducer: Reducer, Sendable { reason: .init(source) )) }, - // todo, launch at login - // .publisher { - // websocket.receive() - // .map { .websocket(.receivedMessage($0)) } - // .receive(on: mainQueue) - // }.cancellable(id: CancelId.websocketMessages), + .exec { _ in + // when we're onboarding, we delay enabling this, to avoid yet another notification + // that might confuse them. it is enabled when they close the onboarding window + if source != .onboardingConnection, (await app.isLaunchAtLoginEnabled()) == false { + await app.enableLaunchAtLogin() + } + }, + .publisher { + websocket.receive() + .map { .websocket(.receivedMessage($0)) } + .receive(on: mainQueue) + }.cancellable(id: CancelId.websocketMessages), .exec { send in var numTicks = 0 for await _ in bgQueue.timer(interval: .seconds(60)) { diff --git a/macapp/App/Sources/App/ApplicationFeature.swift b/macapp/App/Sources/App/ApplicationFeature.swift index 04bdca01..547d7dc4 100644 --- a/macapp/App/Sources/App/ApplicationFeature.swift +++ b/macapp/App/Sources/App/ApplicationFeature.swift @@ -40,13 +40,6 @@ extension ApplicationFeature.RootReducer: RootReducing { } }, - .exec { _ in - // TODO: should be part of onboarding... - // if await app.isLaunchAtLoginEnabled() == false { - // await app.enableLaunchAtLogin() - // } - }, - .publisher { filterExtension.stateChanges() .map { .filter(.receivedState($0)) } diff --git a/macapp/App/Sources/App/CheckInFeature.swift b/macapp/App/Sources/App/CheckInFeature.swift index 3cbf68ed..760e3e15 100644 --- a/macapp/App/Sources/App/CheckInFeature.swift +++ b/macapp/App/Sources/App/CheckInFeature.swift @@ -142,7 +142,7 @@ extension CheckIn { switch source { case .persistence: self = .loadedPersistedUser - case .newConnection: + case .onboardingConnection, .menuBarConnection: self = .userConnected } } diff --git a/macapp/App/Sources/App/HistoryFeature.swift b/macapp/App/Sources/App/HistoryFeature.swift index d53d821f..30e59662 100644 --- a/macapp/App/Sources/App/HistoryFeature.swift +++ b/macapp/App/Sources/App/HistoryFeature.swift @@ -73,11 +73,17 @@ extension HistoryFeature.RootReducer: RootReducing { state.history.userConnection = .established(welcomeDismissed: true) return .none - case .history(.userConnection(.connect(.success(let user)))), - .onboarding(.connectUser(.success(let user))): + case .history(.userConnection(.connect(.success(let user)))): state.user = .init(data: user) return .exec { [persistent = state.persistent] send in - await send(.startProtecting(user: user, from: .newConnection)) + await send(.startProtecting(user: user, from: .menuBarConnection)) + try await storage.savePersistentState(persistent) + } + + case .onboarding(.connectUser(.success(let user))): + state.user = .init(data: user) + return .exec { [persistent = state.persistent] send in + await send(.startProtecting(user: user, from: .onboardingConnection)) try await storage.savePersistentState(persistent) } diff --git a/macapp/App/Sources/App/MonitoringFeature.swift b/macapp/App/Sources/App/MonitoringFeature.swift index e76b009c..39f1f243 100644 --- a/macapp/App/Sources/App/MonitoringFeature.swift +++ b/macapp/App/Sources/App/MonitoringFeature.swift @@ -125,7 +125,9 @@ extension MonitoringFeature.RootReducer { flushKeystrokes(state.filter.isSuspended) ) - case .adminAuthed(.adminWindow(.webview(.disconnectUserClicked))): + case .adminAuthed(.adminWindow(.webview(.disconnectUserClicked))), + .history(.userConnection(.disconnectMissingUser)), + .websocket(.receivedMessage(.userDeleted)): return .cancel(id: CancelId.screenshots) // try to catch the moment when they've fixed monitoring permissions issues diff --git a/macapp/App/Sources/App/Onboarding/OnboardingFeature.swift b/macapp/App/Sources/App/Onboarding/OnboardingFeature.swift index 1f7c91a2..614dad4c 100644 --- a/macapp/App/Sources/App/Onboarding/OnboardingFeature.swift +++ b/macapp/App/Sources/App/Onboarding/OnboardingFeature.swift @@ -54,6 +54,7 @@ struct OnboardingFeature: Feature { ) case connectUser(TaskResult) case setStep(State.Step) + case closeWindow } struct Reducer: FeatureReducer { @@ -335,6 +336,15 @@ struct OnboardingFeature: Feature { state.step = .finish return .none + case .webview(.primaryBtnClicked) where step == .finish, .closeWindow: + state.windowOpen = false + let userConnected = state.connectChildRequest.isSucceeded + return .exec { _ in + if userConnected, (await app.isLaunchAtLoginEnabled()) == false { + await app.enableLaunchAtLogin() + } + } + case .webview(.primaryBtnClicked): // TODO: debug assert, and error log return .none diff --git a/macapp/App/Sources/App/UserConnectionFeature.swift b/macapp/App/Sources/App/UserConnectionFeature.swift index a23599f0..a7d3030c 100644 --- a/macapp/App/Sources/App/UserConnectionFeature.swift +++ b/macapp/App/Sources/App/UserConnectionFeature.swift @@ -22,6 +22,7 @@ enum UserConnectionFeature: Feature { struct RootReducer: RootReducing { @Dependency(\.api) var api + @Dependency(\.app) var app @Dependency(\.storage) var storage @Dependency(\.filterXpc) var xpc } @@ -74,12 +75,17 @@ extension UserConnectionFeature.RootReducer { } } - func disconnectUser(persisting updatedState: Persistent.State) -> Effect { - .exec { send in - await api.clearUserToken() - try await storage.savePersistentState(updatedState) - _ = await xpc.disconnectUser() - } + func disconnectUser(persisting updated: Persistent.State) -> Effect { + .merge( + .exec { _ in + await api.clearUserToken() + try await storage.savePersistentState(updated) + _ = await xpc.disconnectUser() + await app.disableLaunchAtLogin() + }, + .cancel(id: AppReducer.CancelId.heartbeatInterval), + .cancel(id: AppReducer.CancelId.websocketMessages) + ) } } diff --git a/macapp/App/Sources/App/WebSocketFeature.swift b/macapp/App/Sources/App/WebSocketFeature.swift index ce042b90..15cff25d 100644 --- a/macapp/App/Sources/App/WebSocketFeature.swift +++ b/macapp/App/Sources/App/WebSocketFeature.swift @@ -50,6 +50,8 @@ extension WebSocketFeature.RootReducer { case .application(.willSleep), .application(.willTerminate), + .websocket(.receivedMessage(.userDeleted)), + .history(.userConnection(.disconnectMissingUser)), .adminAuthed(.adminWindow(.webview(.confirmQuitAppClicked))), .adminAuthed(.adminWindow(.webview(.disconnectUserClicked))): return .exec { _ in diff --git a/macapp/App/Tests/AppTests/AppReducerTests.swift b/macapp/App/Tests/AppTests/AppReducerTests.swift index b16478b0..ce8f69fa 100644 --- a/macapp/App/Tests/AppTests/AppReducerTests.swift +++ b/macapp/App/Tests/AppTests/AppReducerTests.swift @@ -13,18 +13,16 @@ import XExpect func testDidFinishLaunching_Exhaustive() async { let (store, _) = AppReducer.testStore(exhaustive: true) - let filterSetupSpy = ActorIsolated(false) - store.deps.filterExtension.setup = { - await filterSetupSpy.setValue(true) - return .installedButNotRunning - } - - let tokenSetSpy = ActorIsolated(nil) - store.deps.api.setUserToken = { await tokenSetSpy.setValue($0) } - + let extSetup = mock(always: FilterExtensionState.installedButNotRunning) + store.deps.filterExtension.setup = extSetup.fn + let setUserToken = spy(on: UUID.self, returning: ()) + store.deps.api.setUserToken = setUserToken.fn let filterStateSubject = PassthroughSubject() store.deps.filterExtension.stateChanges = { filterStateSubject.eraseToAnyPublisher() } store.deps.storage.loadPersistentState = { .mock } + store.deps.app.isLaunchAtLoginEnabled = { false } + let enableLaunchAtLogin = mock(always: ()) + store.deps.app.enableLaunchAtLogin = enableLaunchAtLogin.fn await store.send(.application(.didFinishLaunching)) @@ -33,7 +31,7 @@ import XExpect $0.history.userConnection = .established(welcomeDismissed: true) } - await expect(filterSetupSpy).toEqual(true) + await expect(extSetup.invocations).toEqual(1) await store.receive(.filter(.receivedState(.installedButNotRunning))) { $0.filter.extension = .installedButNotRunning @@ -42,7 +40,8 @@ import XExpect await store.receive(.startProtecting(user: .mock, from: .persistence)) await store.receive(.websocket(.connectedSuccessfully)) - await expect(tokenSetSpy).toEqual(UserData.mock.token) + await expect(setUserToken.invocations).toEqual([UserData.mock.token]) + await expect(enableLaunchAtLogin.invocations).toEqual(1) let prevUser = store.state.user.data diff --git a/macapp/App/Tests/AppTests/OnboardingFeatureTests.swift b/macapp/App/Tests/AppTests/OnboardingFeatureTests.swift index 513a30eb..a0a46ee8 100644 --- a/macapp/App/Tests/AppTests/OnboardingFeatureTests.swift +++ b/macapp/App/Tests/AppTests/OnboardingFeatureTests.swift @@ -115,7 +115,7 @@ import XExpect $0.onboarding.connectChildRequest = .succeeded(payload: "lil suzy") } - await store.receive(.startProtecting(user: user, from: .newConnection)) + await store.receive(.startProtecting(user: user, from: .onboardingConnection)) await store.receive(.checkIn(result: .success(checkInResult), reason: .userConnected)) { $0.appUpdates.latestVersion = checkInResult.latestRelease } @@ -253,6 +253,15 @@ import XExpect $0.onboarding.step = .finish // ...and go to finish } + // primary button on finish screen closes window, enables launch at login + store.deps.app.isLaunchAtLoginEnabled = { false } + let enableLaunchAtLogin = mock(always: ()) + store.deps.app.enableLaunchAtLogin = enableLaunchAtLogin.fn + await store.send(.onboarding(.webview(.primaryBtnClicked))) { + $0.onboarding.windowOpen = false + } + await expect(enableLaunchAtLogin.invocations).toEqual(1) + // shutdown tries fo flush keystrokes store.deps.monitoring = .mock store.deps.monitoring.takePendingKeystrokes = { nil } diff --git a/macapp/App/Tests/AppTests/UserConnectionTests.swift b/macapp/App/Tests/AppTests/UserConnectionTests.swift new file mode 100644 index 00000000..4fbdc74a --- /dev/null +++ b/macapp/App/Tests/AppTests/UserConnectionTests.swift @@ -0,0 +1,99 @@ +import ComposableArchitecture +import Core +import TestSupport +import XCTest +import XExpect + +@testable import App + +@MainActor final class UserConnectionTests: XCTestCase { + func testDisconnectingUserFromAdminWindowStateAndEffects() async { + await disconnectTest( + action: .adminAuthed(.adminWindow(.webview(.disconnectUserClicked))), + setupState: { + $0.adminWindow.windowOpen = true + $0.menuBar.dropdownOpen = false + }, + assertState: { + $0.adminWindow.windowOpen = false + $0.menuBar.dropdownOpen = true + } + ) + } + + func testDisconnectingUserFromWebsocketMsgStateAndEffects() async { + await disconnectTest( + action: .websocket(.receivedMessage(.userDeleted)), + extraReceivedActions: [ + .focusedNotification(.text( + "Child deleted", + "The child associated with this computer was deleted. You'll need to connect to a different child, or quit the app." + )), + ] + ) + } + + func testDisconnectingUserFromMissingUserTokenStateAndEffects() async { + await disconnectTest( + action: .history(.userConnection(.disconnectMissingUser)), + extraReceivedActions: [ + .focusedNotification(.text( + "Child deleted", + "The child associated with this computer was deleted. You'll need to connect to a different child, or quit the app." + )), + ] + ) + } +} + +@MainActor func disconnectTest( + action: AppReducer.Action, + setupState: @escaping (inout AppReducer.State) -> Void = { _ in }, + setupStore: (TestStoreOf) -> Void = { _ in }, + assertState: @escaping (inout AppReducer.State) -> Void = { _ in }, + extraReceivedActions: [AppReducer.Action] = [] +) async { + let (store, bgQueue) = AppReducer.testStore { + $0.user.data = .mock + $0.history.userConnection = .established(welcomeDismissed: true) + setupState(&$0) + } + + store.exhaustivity = .on + await store.withExhaustivity(.off) { + await store.send(.startProtecting(user: .mock, from: .persistence)) + await store.skipReceivedActions() + await bgQueue.advance(by: .seconds(60)) + await store.receive(.heartbeat(.everyMinute)) // <-- heartbeat is running + } + + let clearApiToken = mock(always: ()) + store.deps.api.clearUserToken = clearApiToken.fn + let saveState = spy(on: Persistent.State.self, returning: ()) + store.deps.storage.savePersistentState = saveState.fn + let xpcDisconnect = mock(once: Result.success(())) + store.deps.filterXpc.disconnectUser = xpcDisconnect.fn + let disableLaunchAtLogin = mock(always: ()) + store.deps.app.disableLaunchAtLogin = disableLaunchAtLogin.fn + store.deps.monitoring.commitPendingKeystrokes = { _ in fatalError() } + store.deps.monitoring.takeScreenshot = { _ in fatalError() } + setupStore(store) + + await store.send(action) { + $0.user = .init() + $0.history.userConnection = .notConnected + assertState(&$0) + } + + for action in extraReceivedActions { + await store.receive(action) + } + + await expect(clearApiToken.invocations).toEqual(1) + await expect(saveState.invocations.value).toHaveCount(1) + await expect(xpcDisconnect.invocations).toEqual(1) + await expect(disableLaunchAtLogin.invocations).toEqual(1) + + // no heartbeat actions received, no timed screenshots + await bgQueue.advance(by: .seconds(60 * 10)) +} From 3523df89e2386b6c262ed28f33235e5d2f1e2600 Mon Sep 17 00:00:00 2001 From: Jared Henderson Date: Wed, 11 Oct 2023 10:25:17 -0400 Subject: [PATCH 04/25] macapp: first pass at full integration with appview window --- api/Sources/Api/Environment/Ephemeral.swift | 3 + .../Api/Models/Admin/Device+Model.swift | 1 + macapp/App/Sources/App/App.swift | 5 + macapp/App/Sources/App/AppReducer.swift | 4 +- macapp/App/Sources/App/AppWindow.swift | 10 + .../App/Sources/App/ApplicationFeature.swift | 7 + .../DeviceClient/DeviceClient+Os.swift | 69 ++++ .../DeviceClient/DeviceClient.swift | 4 + .../Generated/OnboardingFeature+Codable.swift | 75 ++++ macapp/App/Sources/App/HistoryFeature.swift | 1 + .../App/Onboarding/OnboardingFeature.swift | 313 +++++++++++----- .../App/Onboarding/OnboardingState+View.swift | 24 ++ .../App/Onboarding/OnboardingStep.swift | 202 +++++++++++ .../App/Onboarding/OnboardingWindow.swift | 35 ++ .../RequestSuspensionWindow.swift | 1 + .../App/Sources/App/WebViewController.swift | 16 +- .../App/Tests/AppTests/AppReducerTests.swift | 2 +- .../AppTests/OnboardingFeatureTests.swift | 339 +++++++++++++----- .../Tests/Codegen/AppTypeScriptEnums.swift | 6 + macapp/App/Tests/Codegen/AppWebViews.swift | 20 ++ macapp/App/Tests/Codegen/AppviewStore.swift | 3 + .../Gertrude/WebViews/Administrate/index.js | 4 +- .../Gertrude/WebViews/Administrate/style.css | 2 +- .../WebViews/BlockedRequests/index.js | 4 +- .../WebViews/BlockedRequests/style.css | 2 +- .../Xcode/Gertrude/WebViews/MenuBar/style.css | 2 +- .../WebViews/Onboarding/index.dark.html | 16 + .../Gertrude/WebViews/Onboarding/index.js | 91 +++++ .../WebViews/Onboarding/index.light.html | 16 + .../Gertrude/WebViews/Onboarding/style.css | 1 + .../WebViews/RequestSuspension/index.js | 2 +- .../WebViews/RequestSuspension/style.css | 2 +- 32 files changed, 1097 insertions(+), 185 deletions(-) create mode 100644 macapp/App/Sources/App/Dependencies/DeviceClient/DeviceClient+Os.swift create mode 100644 macapp/App/Sources/App/Generated/OnboardingFeature+Codable.swift create mode 100644 macapp/App/Sources/App/Onboarding/OnboardingState+View.swift create mode 100644 macapp/App/Sources/App/Onboarding/OnboardingStep.swift create mode 100644 macapp/App/Sources/App/Onboarding/OnboardingWindow.swift create mode 100644 macapp/Xcode/Gertrude/WebViews/Onboarding/index.dark.html create mode 100644 macapp/Xcode/Gertrude/WebViews/Onboarding/index.js create mode 100644 macapp/Xcode/Gertrude/WebViews/Onboarding/index.light.html create mode 100644 macapp/Xcode/Gertrude/WebViews/Onboarding/style.css diff --git a/api/Sources/Api/Environment/Ephemeral.swift b/api/Sources/Api/Environment/Ephemeral.swift index 9f2c608b..8d740961 100644 --- a/api/Sources/Api/Environment/Ephemeral.swift +++ b/api/Sources/Api/Environment/Ephemeral.swift @@ -95,6 +95,9 @@ actor Ephemeral { } func getPendingAppConnection(_ code: Int) -> User.Id? { + #if DEBUG + if code == 999_999 { return AdminBetsy.Ids.jimmysId } + #endif guard let (userId, expiration) = pendingAppConnections[code], expiration > Current.date() else { return nil diff --git a/api/Sources/Api/Models/Admin/Device+Model.swift b/api/Sources/Api/Models/Admin/Device+Model.swift index 44cb8e97..fd9c723f 100644 --- a/api/Sources/Api/Models/Admin/Device+Model.swift +++ b/api/Sources/Api/Models/Admin/Device+Model.swift @@ -123,6 +123,7 @@ extension Device.Model.Chip { } enum MacOS: Encodable { + case sonoma case ventura case monterey case bigSur diff --git a/macapp/App/Sources/App/App.swift b/macapp/App/Sources/App/App.swift index c702e621..ae8fede5 100644 --- a/macapp/App/Sources/App/App.swift +++ b/macapp/App/Sources/App/App.swift @@ -10,6 +10,7 @@ typealias UserData = GetUserData.Output var blockedRequestsWindow: BlockedRequestsWindow var adminWindow: AdminWindow var requestSuspensionWindow: RequestSuspensionWindow + var onboardingWindow: OnboardingWindow let store = Store( initialState: AppReducer.State(appVersion: { @Dependency(\.app) var appClient @@ -48,6 +49,10 @@ typealias UserData = GetUserData.Output state: { $0 }, action: AppReducer.Action.requestSuspension )) + onboardingWindow = OnboardingWindow(store: store.scope( + state: { $0 }, + action: AppReducer.Action.onboarding + )) #if !DEBUG setEventReporter { kind, eventId, detail in diff --git a/macapp/App/Sources/App/AppReducer.swift b/macapp/App/Sources/App/AppReducer.swift index ddecc69e..7ee232c4 100644 --- a/macapp/App/Sources/App/AppReducer.swift +++ b/macapp/App/Sources/App/AppReducer.swift @@ -164,10 +164,10 @@ struct AppReducer: Reducer, Sendable { } } - case .onboarding(.delegate(.saveCurrentStep(let step))): + case .onboarding(.delegate(.saveForResume(let resume))): return .exec { [persist = state.persistent] _ in var copy = persist - copy.resumeOnboarding = step.map { .at(step: $0) } + copy.resumeOnboarding = resume try await storage.savePersistentState(copy) } diff --git a/macapp/App/Sources/App/AppWindow.swift b/macapp/App/Sources/App/AppWindow.swift index 6481a267..36ff2d32 100644 --- a/macapp/App/Sources/App/AppWindow.swift +++ b/macapp/App/Sources/App/AppWindow.swift @@ -20,6 +20,7 @@ protocol AppWindow: AnyObject { var windowLevel: NSWindow.Level { get } var title: String { get } var screen: String { get } + var showTitleBar: Bool { get } var closeWindowAction: Action { get } func embed(_ webviewAction: WebViewAction) -> Action @@ -29,6 +30,7 @@ extension AppWindow { var windowLevel: NSWindow.Level { .normal } var initialSize: NSRect { NSRect(x: 0, y: 0, width: 900, height: 600) } var minSize: NSSize { NSSize(width: 800, height: 500) } + var showTitleBar: Bool { true } @MainActor func bind() { windowDelegate.events @@ -69,10 +71,18 @@ extension AppWindow { window?.delegate = windowDelegate window?.tabbingMode = .disallowed window?.titlebarAppearsTransparent = true + + if !showTitleBar { + window?.titleVisibility = .hidden + window?.styleMask.insert(NSWindow.StyleMask.fullSizeContentView) + window?.isMovableByWindowBackground = true + } + window?.isReleasedWhenClosed = false window?.level = windowLevel let wvc = WebViewController() + wvc.withTitleBar = showTitleBar wvc.send = { [weak self] action in guard let self = self else { return } diff --git a/macapp/App/Sources/App/ApplicationFeature.swift b/macapp/App/Sources/App/ApplicationFeature.swift index 547d7dc4..87b1ed5a 100644 --- a/macapp/App/Sources/App/ApplicationFeature.swift +++ b/macapp/App/Sources/App/ApplicationFeature.swift @@ -1,5 +1,7 @@ import ComposableArchitecture +import Foundation + // public, not nested, because it's used in the AppDelegate public enum ApplicationAction: Equatable, Sendable { case didFinishLaunching @@ -29,6 +31,11 @@ extension ApplicationFeature.RootReducer: RootReducing { case .application(.didFinishLaunching): return .merge( .exec { send in + #if DEBUG + if ProcessInfo.processInfo.environment["SWIFT_DETERMINISTIC_HASHING"] == nil { + await storage.deleteAll() + } + #endif await send(.loadedPersistentState(try await storage.loadPersistentState())) }, diff --git a/macapp/App/Sources/App/Dependencies/DeviceClient/DeviceClient+Os.swift b/macapp/App/Sources/App/Dependencies/DeviceClient/DeviceClient+Os.swift new file mode 100644 index 00000000..a524913f --- /dev/null +++ b/macapp/App/Sources/App/Dependencies/DeviceClient/DeviceClient+Os.swift @@ -0,0 +1,69 @@ +import Foundation + +struct MacOSVersion: Sendable { + enum Name: String { + case catalina + case bigSur + case monterey + case ventura + case sonoma + case next + } + + let major: Int + let minor: Int + let patch: Int + + var semver: String { + "\(major).\(minor).\(patch)" + } + + var name: Name { + switch (major, minor) { + case (10, 15): return .catalina + case (11, _): return .bigSur + case (12, _): return .monterey + case (13, _): return .ventura + case (14, _): return .sonoma + default: return .next + } + } + + var description: String { + "\(name.rawValue)@\(semver)" + } +} + +@Sendable func macOSVersion() -> MacOSVersion { + let version = ProcessInfo.processInfo.operatingSystemVersion + return MacOSVersion( + major: version.majorVersion, + minor: version.minorVersion, + patch: version.patchVersion + ) +} + +extension MacOSVersion { + enum DocumentationGroup: String, Encodable { + case catalina + case bigSurOrMonterey + case venturaOrLater + } + + var documentationGroup: DocumentationGroup { + switch name { + case .catalina: + return .catalina + case .bigSur, .monterey: + return .bigSurOrMonterey + case .ventura, .sonoma, .next: + return .venturaOrLater + } + } +} + +#if DEBUG + extension MacOSVersion { + static let sonoma = Self(major: 14, minor: 0, patch: 0) + } +#endif diff --git a/macapp/App/Sources/App/Dependencies/DeviceClient/DeviceClient.swift b/macapp/App/Sources/App/Dependencies/DeviceClient/DeviceClient.swift index 301d6b45..dde14f8a 100644 --- a/macapp/App/Sources/App/Dependencies/DeviceClient/DeviceClient.swift +++ b/macapp/App/Sources/App/Dependencies/DeviceClient/DeviceClient.swift @@ -12,6 +12,7 @@ struct DeviceClient: Sendable { var numericUserId: @Sendable () -> uid_t var openSystemPrefs: @Sendable (SystemPrefsLocation) async -> Void var openWebUrl: @Sendable (URL) async -> Void + var osVersion: @Sendable () -> MacOSVersion var quitBrowsers: @Sendable () async -> Void var requestNotificationAuthorization: @Sendable () async -> Void var showNotification: @Sendable (String, String) async -> Void @@ -30,6 +31,7 @@ extension DeviceClient: DependencyKey { numericUserId: { getuid() }, openSystemPrefs: openSystemPrefs(at:), openWebUrl: { NSWorkspace.shared.open($0) }, + osVersion: { macOSVersion() }, quitBrowsers: quitAllBrowsers, requestNotificationAuthorization: requestNotificationAuth, showNotification: showNotification(title:body:), @@ -49,6 +51,7 @@ extension DeviceClient: TestDependencyKey { numericUserId: unimplemented("DeviceClient.numericUserId"), openSystemPrefs: unimplemented("DeviceClient.openSystemPrefs"), openWebUrl: unimplemented("DeviceClient.openWebUrl"), + osVersion: unimplemented("DeviceClient.osVersion"), quitBrowsers: unimplemented("DeviceClient.quitBrowsers"), requestNotificationAuthorization: unimplemented( "DeviceClient.requestNotificationAuthorization" @@ -71,6 +74,7 @@ extension DeviceClient: TestDependencyKey { numericUserId: { 502 }, openSystemPrefs: { _ in }, openWebUrl: { _ in }, + osVersion: { .init(major: 14, minor: 0, patch: 0) }, quitBrowsers: {}, requestNotificationAuthorization: {}, showNotification: { _, _ in }, diff --git a/macapp/App/Sources/App/Generated/OnboardingFeature+Codable.swift b/macapp/App/Sources/App/Generated/OnboardingFeature+Codable.swift new file mode 100644 index 00000000..bf6e1b7a --- /dev/null +++ b/macapp/App/Sources/App/Generated/OnboardingFeature+Codable.swift @@ -0,0 +1,75 @@ +// auto-generated, do not edit +import Foundation + +extension OnboardingFeature.Action.View { + private struct _NamedCase: Codable { + var `case`: String + static func extract(from decoder: Decoder) throws -> String { + let container = try decoder.singleValueContainer() + return try container.decode(_NamedCase.self).case + } + } + + private struct _TypeScriptDecodeError: Error { + var message: String + } + + private struct _CaseConnectChildSubmitted: Codable { + var `case` = "connectChildSubmitted" + var code: Int + } + + private struct _CaseInfoModalOpened: Codable { + var `case` = "infoModalOpened" + var step: OnboardingFeature.State.Step + var detail: String? + } + + func encode(to encoder: Encoder) throws { + switch self { + case .connectChildSubmitted(let code): + try _CaseConnectChildSubmitted(code: code).encode(to: encoder) + case .infoModalOpened(let step, let detail): + try _CaseInfoModalOpened(step: step, detail: detail).encode(to: encoder) + case .closeWindow: + try _NamedCase(case: "closeWindow").encode(to: encoder) + case .primaryBtnClicked: + try _NamedCase(case: "primaryBtnClicked").encode(to: encoder) + case .secondaryBtnClicked: + try _NamedCase(case: "secondaryBtnClicked").encode(to: encoder) + case .chooseSwitchToNonAdminUserClicked: + try _NamedCase(case: "chooseSwitchToNonAdminUserClicked").encode(to: encoder) + case .chooseCreateNonAdminClicked: + try _NamedCase(case: "chooseCreateNonAdminClicked").encode(to: encoder) + case .chooseDemoteAdminClicked: + try _NamedCase(case: "chooseDemoteAdminClicked").encode(to: encoder) + } + } + + init(from decoder: Decoder) throws { + let caseName = try _NamedCase.extract(from: decoder) + let container = try decoder.singleValueContainer() + switch caseName { + case "connectChildSubmitted": + let value = try container.decode(_CaseConnectChildSubmitted.self) + self = .connectChildSubmitted(code: value.code) + case "infoModalOpened": + let value = try container.decode(_CaseInfoModalOpened.self) + self = .infoModalOpened(step: value.step, detail: value.detail) + case "closeWindow": + self = .closeWindow + case "primaryBtnClicked": + self = .primaryBtnClicked + case "secondaryBtnClicked": + self = .secondaryBtnClicked + case "chooseSwitchToNonAdminUserClicked": + self = .chooseSwitchToNonAdminUserClicked + case "chooseCreateNonAdminClicked": + self = .chooseCreateNonAdminClicked + case "chooseDemoteAdminClicked": + self = .chooseDemoteAdminClicked + default: + throw _TypeScriptDecodeError(message: "Unexpected case name: `\(caseName)`") + } + } +} diff --git a/macapp/App/Sources/App/HistoryFeature.swift b/macapp/App/Sources/App/HistoryFeature.swift index 30e59662..b4bacde4 100644 --- a/macapp/App/Sources/App/HistoryFeature.swift +++ b/macapp/App/Sources/App/HistoryFeature.swift @@ -82,6 +82,7 @@ extension HistoryFeature.RootReducer: RootReducing { case .onboarding(.connectUser(.success(let user))): state.user = .init(data: user) + state.history.userConnection = .established(welcomeDismissed: true) return .exec { [persistent = state.persistent] send in await send(.startProtecting(user: user, from: .onboardingConnection)) try await storage.savePersistentState(persistent) diff --git a/macapp/App/Sources/App/Onboarding/OnboardingFeature.swift b/macapp/App/Sources/App/Onboarding/OnboardingFeature.swift index 614dad4c..885cc19a 100644 --- a/macapp/App/Sources/App/Onboarding/OnboardingFeature.swift +++ b/macapp/App/Sources/App/Onboarding/OnboardingFeature.swift @@ -1,3 +1,4 @@ +import ClientInterfaces import ComposableArchitecture import Foundation @@ -20,7 +21,6 @@ struct OnboardingFeature: Feature { var step: Step = .welcome var userRemediationStep: MacUser.RemediationStep? var currentUser: MacUser? - var existingNotificationsSetting: NotificationsSetting? var connectChildRequest: PayloadRequestState = .idle var users: [MacUser] = [] } @@ -31,27 +31,25 @@ struct OnboardingFeature: Feature { } enum Action: Equatable, Sendable { - enum Webview: Equatable, Sendable { + enum View: Equatable, Sendable, Decodable { + case closeWindow case primaryBtnClicked case secondaryBtnClicked case chooseSwitchToNonAdminUserClicked case chooseCreateNonAdminClicked case chooseDemoteAdminClicked - case connectChildSubmitted(Int) + case connectChildSubmitted(code: Int) + case infoModalOpened(step: State.Step, detail: String?) } enum Delegate: Equatable, Sendable { - case saveCurrentStep(State.Step?) + case saveForResume(Resume?) } - case webview(Webview) + case webview(View) case delegate(Delegate) case resume(Resume) - case receivedDeviceData( - currentUserId: uid_t, - users: [MacOSUser], - notificationsSetting: NotificationsSetting - ) + case receivedDeviceData(currentUserId: uid_t, users: [MacOSUser]) case connectUser(TaskResult) case setStep(State.Step) case closeWindow @@ -74,78 +72,98 @@ struct OnboardingFeature: Feature { case .resume(.at(let step)): state.windowOpen = true state.step = step + log("resuming at \(step)", "711355aa") return .none case .resume(.checkingScreenRecordingPermission): return .exec { send in + let granted = await monitoring.screenRecordingPermissionGranted() + log("resume checking screen recording, granted=\(granted)", "5d1d27fe") await send(.setStep( - await monitoring.screenRecordingPermissionGranted() + granted ? .allowScreenshots_success : .allowScreenshots_failed )) } - case .receivedDeviceData(let currentUserId, let users, let notificationsSetting): + case .receivedDeviceData(let currentUserId, let users): state.users = users.map(State.MacUser.init) state.currentUser = state.users.first(where: { $0.id == currentUserId }) - state.existingNotificationsSetting = notificationsSetting return .none case .webview(.primaryBtnClicked) where step == .welcome: + log(step, action, "e712e261") state.step = .confirmGertrudeAccount return .exec { send in await send(.receivedDeviceData( currentUserId: device.currentUserId(), - users: try await device.listMacOSUsers(), - notificationsSetting: await device.notificationsSetting() + users: try await device.listMacOSUsers() )) } case .webview(.primaryBtnClicked) where step == .confirmGertrudeAccount: + log(step, action, "36a1852c") state.step = .macosUserAccountType return .none case .webview(.secondaryBtnClicked) where step == .confirmGertrudeAccount: + log(step, action, "85958bee") state.step = .noGertrudeAccount return .none + case .webview(.primaryBtnClicked) where step == .noGertrudeAccount: + log(step, action, "05820945") + state.step = .macosUserAccountType + return .none + case .webview(.secondaryBtnClicked) where step == .noGertrudeAccount: + log("quit from no gertrude acct", "236defcb") return .exec { _ in await storage.deleteAll() await app.quit() } case .webview(.primaryBtnClicked) where step == .macosUserAccountType && !userIsAdmin: + log("macos account type correct next clicked", "0a29be72") state.step = .getChildConnectionCode return .none // they choose to ignore the warning about user type and proceed case .webview(.secondaryBtnClicked) where step == .macosUserAccountType && userIsAdmin: + log("skip admin user account warning", "d044eb17") state.step = .getChildConnectionCode return .none // they click "show me how to fix" on the BAD mac os user landing page case .webview(.primaryBtnClicked) where step == .macosUserAccountType && userIsAdmin: state.userRemediationStep = state.users.count == 1 ? .create : .choose - return .send(.delegate(.saveCurrentStep(.macosUserAccountType))) + log("show me how to fix admin user clicked, \(state.users.count) users", "74179c5c") + return .exec { send in + await send(.delegate(.saveForResume(.at(step: .macosUserAccountType)))) + } case .webview(.chooseDemoteAdminClicked): + log(step, action, "d638fa96") state.userRemediationStep = .demote return .none case .webview(.chooseCreateNonAdminClicked): + log(step, action, "c63bf016") state.userRemediationStep = .create return .none case .webview(.chooseSwitchToNonAdminUserClicked): + log(step, action, "68fdb44a") state.userRemediationStep = .switch return .none case .webview(.primaryBtnClicked) where step == .getChildConnectionCode: + log(step, action, "550d9504") state.step = .connectChild return .none case .webview(.connectChildSubmitted(let code)): + log(step, action, "3d6b89a8") state.connectChildRequest = .ongoing return .exec { send in await send(.connectUser((TaskResult { @@ -154,33 +172,38 @@ struct OnboardingFeature: Feature { } case .connectUser(.success(let user)): + log("connect user success", "3a1ac301") state.connectChildRequest = .succeeded(payload: user.name) return .none case .connectUser(.failure(let error)): + log("connect user failed \(error)", "0ed97f9a") state.connectChildRequest = .failed(error: error.userMessage()) return .none case .webview(.primaryBtnClicked) where step == .connectChild && state.connectChildRequest.isFailed: + log("retry connect user", "c69844b8") state.connectChildRequest = .idle state.step = .getChildConnectionCode return .none case .webview(.secondaryBtnClicked) where step == .connectChild && state.connectChildRequest.isFailed: + log("connect user failed secondary", "08de43c1") return .exec { _ in await device.openWebUrl(.contact) } case .webview(.primaryBtnClicked) where step == .connectChild && state.connectChildRequest.isSucceeded: - state.step = state.existingNotificationsSetting == .alert - ? .allowScreenshots_required - : .allowNotifications_start - return .none + log("next from connect user success", "34221891") + return .exec { send in + await send(.setStep(await nextRequiredStage(from: .connectChild))) + } case .webview(.primaryBtnClicked) where step == .allowNotifications_start: + log(step, action, "b183d96d") state.step = .allowNotifications_grant return .exec { _ in await device.requestNotificationAuthorization() @@ -189,89 +212,164 @@ struct OnboardingFeature: Feature { case .webview(.primaryBtnClicked) where step == .allowNotifications_grant || step == .allowNotifications_failed: + log(step, action, "9fa094ac") return .exec { send in await send(.setStep( await device.notificationsSetting() != .none - ? .allowScreenshots_required + ? await nextRequiredStage(from: step) : .allowNotifications_failed )) } + case .webview(.secondaryBtnClicked) where step == .allowNotifications_grant: + log(step, action, "8f9d3c9c") + state.step = .allowNotifications_failed + return .none + case .webview(.secondaryBtnClicked) where step == .allowNotifications_start || step == .allowNotifications_failed: - state.step = .allowScreenshots_required - return .none + log(step, action, "8cf52d46") + return .exec { send in + await send(.setStep(await nextRequiredStage(from: step))) + } case .webview(.primaryBtnClicked) where step == .allowScreenshots_required: return .exec { send in + let granted = await monitoring.screenRecordingPermissionGranted() + log("primary from .allowScreenshots_required, already granted=\(granted)", "ce78b67b") await send(.setStep( - await monitoring.screenRecordingPermissionGranted() - ? .allowKeylogging_required + granted + ? await nextRequiredStage(from: step) : .allowScreenshots_openSysSettings )) } case .webview(.secondaryBtnClicked) where step == .allowScreenshots_required: - state.step = .allowKeylogging_required - return .none + log(step, action, "b2907efa") + return .exec { send in + await send(.setStep(await nextRequiredStage(from: step))) + } case .webview(.primaryBtnClicked) where step == .allowScreenshots_openSysSettings: + log(step, action, "4e52e7d8") state.step = .allowScreenshots_grantAndRestart - return .none + return .exec { send in + await send(.delegate(.saveForResume(.checkingScreenRecordingPermission))) + } - case .webview(.primaryBtnClicked) where step == .allowScreenshots_failed: + case .webview(.secondaryBtnClicked) where step == .allowScreenshots_openSysSettings: + log(step, action, "2d2e6a2f") state.step = .allowScreenshots_grantAndRestart - return .exec { _ in + return .exec { send in await device.openSystemPrefs(.security(.screenRecording)) + await send(.delegate(.saveForResume(.checkingScreenRecordingPermission))) } - case .webview(.secondaryBtnClicked) where step == .allowScreenshots_failed: - state.step = .allowKeylogging_required + case .webview(.primaryBtnClicked) where step == .allowScreenshots_grantAndRestart: + log(step, action, "c7e2bed4") + state.step = .allowScreenshots_failed return .none + case .webview(.secondaryBtnClicked) where step == .allowScreenshots_grantAndRestart: + log(step, action, "a85b700c") + return .exec { send in + await send(.setStep(nextRequiredStage(from: step))) + } + + case .webview(.primaryBtnClicked) where step == .allowScreenshots_failed: + log(step, action, "cfb65d32") + return .exec { send in + if await monitoring.screenRecordingPermissionGranted() { + await send(.setStep(.allowScreenshots_success)) + } else { + await device.openSystemPrefs(.security(.screenRecording)) + await send(.setStep(.allowScreenshots_grantAndRestart)) + } + } + + case .webview(.secondaryBtnClicked) where step == .allowScreenshots_failed: + log(step, action, "9616ea42") + return .exec { send in + await send(.setStep(await nextRequiredStage(from: step))) + } + case .webview(.primaryBtnClicked) where step == .allowScreenshots_success: - state.step = .allowKeylogging_required - return .none + log(step, action, "fc9a6916") + return .exec { send in + await send(.setStep(await nextRequiredStage(from: step))) + } case .webview(.primaryBtnClicked) where step == .allowKeylogging_required: return .exec { send in + let granted = await monitoring.keystrokeRecordingPermissionGranted() + log("primary from .allowKeylogging_required, already granted=\(granted)", "ce78b67b") await send(.setStep( - await monitoring.keystrokeRecordingPermissionGranted() - ? .installSysExt_explain + granted + ? await nextRequiredStage(from: step) : .allowKeylogging_openSysSettings )) } case .webview(.secondaryBtnClicked) where step == .allowKeylogging_required: - state.step = .installSysExt_explain - return .none + log(step, action, "61a87bb2") + return .exec { send in + await send(.setStep(await nextRequiredStage(from: step))) + } case .webview(.primaryBtnClicked) where step == .allowKeylogging_openSysSettings: + log(step, action, "c2e08e19") state.step = .allowKeylogging_grant return .none case .webview(.primaryBtnClicked) where step == .allowKeylogging_grant: return .exec { send in + let granted = await monitoring.keystrokeRecordingPermissionGranted() + log("primary from .allowKeylogging_grant, granted=\(granted)", "ce78b67b") await send(.setStep( - await monitoring.keystrokeRecordingPermissionGranted() - ? .installSysExt_explain + granted + ? await nextRequiredStage(from: step) : .allowKeylogging_failed )) } + case .webview(.secondaryBtnClicked) where step == .allowKeylogging_grant: + log(step, action, "5ccce8b9") + return .exec { send in + if await monitoring.keystrokeRecordingPermissionGranted() { + await send(.setStep(nextRequiredStage(from: step))) + } else { + await send(.setStep(.allowKeylogging_failed)) + } + } + case .webview(.primaryBtnClicked) where step == .allowKeylogging_failed: - state.step = .allowKeylogging_grant - return .exec { _ in - await device.openSystemPrefs(.security(.accessibility)) + log(step, action, "36181833") + return .exec { send in + if await monitoring.keystrokeRecordingPermissionGranted() { + await send(.setStep(nextRequiredStage(from: step))) + } else { + await device.openSystemPrefs(.security(.accessibility)) + await send(.setStep(.allowKeylogging_grant)) + } + } + + case .webview(.secondaryBtnClicked) where step == .allowKeylogging_failed: + log(step, action, "775f57f9") + return .exec { send in + await send(.setStep(await nextRequiredStage(from: step))) } case .webview(.primaryBtnClicked) where step == .installSysExt_explain: return .exec { send in - switch await systemExtension.state() { + let startingState = await systemExtension.state() + log("primary from .installSysExt_explain, state=\(startingState)", "e585331d") + switch startingState { case .notInstalled: await send(.setStep(.installSysExt_allow)) try? await mainQueue.sleep(for: .seconds(3)) // let them see the explanation gif - switch await systemExtension.install() { + let installResult = await systemExtension.install() + log("sys ext install result=\(installResult)", "adbc0453") + switch installResult { case .installedSuccessfully: await send(.setStep(.installSysExt_success)) case .timedOutWaiting, .userClickedDontAllow: @@ -291,6 +389,7 @@ struct OnboardingFeature: Feature { await send(.setStep(.installSysExt_success)) case .installedButNotRunning: if await systemExtension.start() == .installedAndRunning { + log("non-running sys ext started successfully", "d0021f5d") await send(.setStep(.installSysExt_success)) } else { // TODO: should we try to replace once? @@ -299,46 +398,52 @@ struct OnboardingFeature: Feature { } } - case .webview(.primaryBtnClicked) where step == .installSysExt_allow: + case .webview(.primaryBtnClicked) where step == .installSysExt_allow, + .webview(.secondaryBtnClicked) where step == .installSysExt_allow: return .exec { send in - if await systemExtension.state() == .installedAndRunning { + let state = await systemExtension.state() + log("\(action) from .installSysExt_allow, state=\(state)", "b0e6e683") + if state == .installedAndRunning { await send(.setStep(.installSysExt_success)) } else { await send(.setStep(.installSysExt_failed)) } } - case .webview(.secondaryBtnClicked) where step == .installSysExt_allow: - state.step = .installSysExt_failed - return .none - case .webview(.primaryBtnClicked) where step == .installSysExt_failed: + log(step, action, "2e246f1d") state.step = .installSysExt_explain return .none case .webview(.secondaryBtnClicked) where step == .installSysExt_failed: + log(step, action, "78bded66") state.step = .locateMenuBarIcon return .none case .webview(.primaryBtnClicked) where step == .installSysExt_success: + log(step, action, "7009a9cf") state.step = .locateMenuBarIcon return .none case .webview(.primaryBtnClicked) where step == .locateMenuBarIcon: + log(step, action, "d0a159fd") state.step = .viewHealthCheck return .none case .webview(.primaryBtnClicked) where step == .viewHealthCheck: + log(step, action, "5c73a171") state.step = .howToUseGertrude return .none case .webview(.primaryBtnClicked) where step == .howToUseGertrude: + log(step, action, "eb044990") state.step = .finish return .none - case .webview(.primaryBtnClicked) where step == .finish, .closeWindow: + case .webview(.primaryBtnClicked) where step == .finish, .closeWindow, .webview(.closeWindow): state.windowOpen = false let userConnected = state.connectChildRequest.isSucceeded + log("\(action), step=\(step), userConnected=\(userConnected)", "936082d4") return .exec { _ in if userConnected, (await app.isLaunchAtLoginEnabled()) == false { await app.enableLaunchAtLogin() @@ -346,11 +451,15 @@ struct OnboardingFeature: Feature { } case .webview(.primaryBtnClicked): - // TODO: debug assert, and error log + assertionFailure("Unhandled primary button click") + unexpectedError(id: "56bce346", detail: "step: \(step)") + state.step = step.primaryFallbackNextStep return .none case .webview(.secondaryBtnClicked): - // TODO: debug assert, and error log + assertionFailure("Unhandled secondary button click") + unexpectedError(id: "22bfde1a", detail: "step: \(step)") + state.step = step.secondaryFallbackNextStep return .none case .setStep(let step): @@ -358,47 +467,48 @@ struct OnboardingFeature: Feature { state.windowOpen = true // for resuming return .none + case .webview(.infoModalOpened(let step, let detail)): + log("info modal opened at .\(step), detail=\(detail ?? "(nil)")", "f77ef50c") + return .none + case .delegate: return .none } } - } -} -extension OnboardingFeature.State { - enum Step: Equatable, Codable { - case welcome - case confirmGertrudeAccount - case noGertrudeAccount - case macosUserAccountType - case getChildConnectionCode - case connectChild - case allowNotifications_start - case allowNotifications_grant - case allowNotifications_failed - case allowScreenshots_required - case allowScreenshots_openSysSettings - case allowScreenshots_grantAndRestart - - // these two states exist to give us a landing spot for resuming - // onboarding after the grant -> quit & reopen flow - case allowScreenshots_failed - case allowScreenshots_success - - case allowKeylogging_required - case allowKeylogging_openSysSettings - case allowKeylogging_grant - case allowKeylogging_failed - - case installSysExt_explain - case installSysExt_allow - case installSysExt_failed - case installSysExt_success - - case locateMenuBarIcon - case viewHealthCheck - case howToUseGertrude - case finish + func nextRequiredStage(from current: State.Step) async -> State.Step { + if current < .allowNotifications_start { + if await device.notificationsSetting() != .alert { + log("notifications not .alert yet", "ec99a6ea") + return .allowNotifications_start + } + log("notifications already .alert, skipping stage", "f2988b3c") + } + + if current < .allowScreenshots_required { + if await monitoring.screenRecordingPermissionGranted() == false { + log("screen recording not granted yet", "3edcf34f") + return .allowScreenshots_required + } + log("screenshots already allowed, skipping stage", "6e2e204c") + } + + if current < .allowKeylogging_required { + if await monitoring.keystrokeRecordingPermissionGranted() == false { + log("keylogging not granted yet", "5d5275e5") + return .allowKeylogging_required + } + log("keylogging already allowed, skipping stage", "51ed2be8") + } + + if await systemExtension.state() != .installedAndRunning { + log("sys ext not installed and running yet", "b493ebde") + return .installSysExt_explain + } + + log("sys ext already installed and running, skipping stage", "b0e6e683") + return .locateMenuBarIcon + } } } @@ -409,3 +519,26 @@ extension OnboardingFeature.State.MacUser { isAdmin = user.type == .admin } } + +extension OnboardingFeature.Reducer { + + func eventMeta() -> String { + "os: \(device.osVersion().name), sn: \(device.serialNumber() ?? ""), time: \(Date())" + } + + func log(_ msg: String, _ id: String) { + #if !DEBUG + Task { interestingEvent(id: id, "[onboarding]: \(msg), \(eventMeta())") } + #else + if ProcessInfo.processInfo.environment["SWIFT_DETERMINISTIC_HASHING"] == nil { + print("\n[onboarding]: `\(id)` \(msg), \(eventMeta())\n") + } + #endif + } + + func log(_ step: State.Step, _ action: Action, _ id: String) { + let shortAction = "\(action)" + .replacingOccurrences(of: "App.OnboardingFeature.Action.View", with: "") + log("received .\(shortAction) from step .\(step)", id) + } +} diff --git a/macapp/App/Sources/App/Onboarding/OnboardingState+View.swift b/macapp/App/Sources/App/Onboarding/OnboardingState+View.swift new file mode 100644 index 00000000..bfe40d80 --- /dev/null +++ b/macapp/App/Sources/App/Onboarding/OnboardingState+View.swift @@ -0,0 +1,24 @@ +import Dependencies + +extension OnboardingFeature.State { + struct View: Equatable, Encodable, Sendable { + var os: MacOSVersion.DocumentationGroup + var windowOpen: Bool + var step: Step + var userRemediationStep: MacUser.RemediationStep? + var currentUser: MacUser? + var connectChildRequest: PayloadRequestState + var users: [MacUser] + + init(state: AppReducer.State) { + @Dependency(\.device) var device + os = device.osVersion().documentationGroup + windowOpen = state.onboarding.windowOpen + step = state.onboarding.step + userRemediationStep = state.onboarding.userRemediationStep + currentUser = state.onboarding.currentUser + connectChildRequest = state.onboarding.connectChildRequest + users = state.onboarding.users + } + } +} diff --git a/macapp/App/Sources/App/Onboarding/OnboardingStep.swift b/macapp/App/Sources/App/Onboarding/OnboardingStep.swift new file mode 100644 index 00000000..a670ae0c --- /dev/null +++ b/macapp/App/Sources/App/Onboarding/OnboardingStep.swift @@ -0,0 +1,202 @@ + +extension OnboardingFeature.State { + enum Step: String, Equatable, Codable { + case welcome + + // account + case confirmGertrudeAccount + case noGertrudeAccount + + // os user type + case macosUserAccountType + + // connection + case getChildConnectionCode + case connectChild + + // notifications + case allowNotifications_start + case allowNotifications_grant + case allowNotifications_failed + + // screenshots + case allowScreenshots_required + case allowScreenshots_openSysSettings + case allowScreenshots_grantAndRestart + // these two states exist to give us a landing spot for resuming + // onboarding after the grant -> quit & reopen flow + case allowScreenshots_failed + case allowScreenshots_success + + // keylogging + case allowKeylogging_required + case allowKeylogging_openSysSettings + case allowKeylogging_grant + case allowKeylogging_failed + + // sys ext + case installSysExt_explain + case installSysExt_allow + case installSysExt_failed + case installSysExt_success + + // wrap up + case locateMenuBarIcon + case viewHealthCheck + case howToUseGertrude + case finish + } +} + +extension OnboardingFeature.State.Step { + var primaryFallbackNextStep: Self { + switch self { + case .welcome: + return .confirmGertrudeAccount + case .confirmGertrudeAccount: + return .macosUserAccountType + case .noGertrudeAccount: + return .macosUserAccountType + case .macosUserAccountType: + return .getChildConnectionCode + case .getChildConnectionCode: + return .connectChild + case .connectChild: + return .allowNotifications_start + case .allowNotifications_start: + return .allowNotifications_grant + case .allowNotifications_grant: + return .allowScreenshots_required + case .allowNotifications_failed: + return .allowScreenshots_required + case .allowScreenshots_required: + return .allowScreenshots_openSysSettings + case .allowScreenshots_openSysSettings: + return .allowScreenshots_grantAndRestart + case .allowScreenshots_grantAndRestart: + return .allowScreenshots_success + case .allowScreenshots_failed: + return .allowKeylogging_required + case .allowScreenshots_success: + return .allowKeylogging_required + case .allowKeylogging_required: + return .allowKeylogging_openSysSettings + case .allowKeylogging_openSysSettings: + return .allowKeylogging_grant + case .allowKeylogging_grant: + return .installSysExt_explain + case .allowKeylogging_failed: + return .installSysExt_explain + case .installSysExt_explain: + return .installSysExt_allow + case .installSysExt_allow: + return .installSysExt_success + case .installSysExt_failed: + return .installSysExt_success + case .installSysExt_success: + return .locateMenuBarIcon + case .locateMenuBarIcon: + return .viewHealthCheck + case .viewHealthCheck: + return .howToUseGertrude + case .howToUseGertrude: + return .finish + case .finish: + return .finish + } + } + + var secondaryFallbackNextStep: Self { + switch self { + case .welcome: + return .welcome + case .confirmGertrudeAccount: + return .welcome + case .noGertrudeAccount: + return .confirmGertrudeAccount + case .macosUserAccountType: + return .confirmGertrudeAccount + case .getChildConnectionCode: + return .macosUserAccountType + case .connectChild: + return .getChildConnectionCode + case .allowNotifications_start: + return .connectChild + case .allowNotifications_grant: + return .allowNotifications_start + case .allowNotifications_failed: + return .allowNotifications_grant + case .allowScreenshots_required: + return .allowNotifications_start + case .allowScreenshots_openSysSettings: + return .allowScreenshots_required + case .allowScreenshots_grantAndRestart: + return .allowScreenshots_openSysSettings + case .allowScreenshots_failed: + return .allowScreenshots_required + case .allowScreenshots_success: + return .allowScreenshots_grantAndRestart + case .allowKeylogging_required: + return .allowScreenshots_required + case .allowKeylogging_openSysSettings: + return .allowKeylogging_required + case .allowKeylogging_grant: + return .allowKeylogging_openSysSettings + case .allowKeylogging_failed: + return .allowKeylogging_required + case .installSysExt_explain: + return .allowKeylogging_required + case .installSysExt_allow: + return .installSysExt_explain + case .installSysExt_failed: + return .installSysExt_explain + case .installSysExt_success: + return .installSysExt_explain + case .locateMenuBarIcon: + return .installSysExt_explain + case .viewHealthCheck: + return .locateMenuBarIcon + case .howToUseGertrude: + return .viewHealthCheck + case .finish: + return .howToUseGertrude + } + } +} + +extension OnboardingFeature.State.Step: Comparable { + public static func < (lhs: Self, rhs: Self) -> Bool { + lhs.asInt < rhs.asInt + } + + private var asInt: Int { + switch self { + case .welcome: return 0 + case .confirmGertrudeAccount: return 5 + case .noGertrudeAccount: return 10 + case .macosUserAccountType: return 15 + case .getChildConnectionCode: return 20 + case .connectChild: return 25 + case .allowNotifications_start: return 30 + case .allowNotifications_grant: return 35 + case .allowNotifications_failed: return 40 + case .allowScreenshots_required: return 45 + case .allowScreenshots_openSysSettings: return 50 + case .allowScreenshots_grantAndRestart: return 55 + case .allowScreenshots_failed: return 60 + case .allowScreenshots_success: return 65 + case .allowKeylogging_required: return 70 + case .allowKeylogging_openSysSettings: return 75 + case .allowKeylogging_grant: return 80 + case .allowKeylogging_failed: return 85 + case .installSysExt_explain: return 90 + case .installSysExt_allow: return 95 + case .installSysExt_failed: return 100 + case .installSysExt_success: return 105 + case .locateMenuBarIcon: return 110 + case .viewHealthCheck: return 115 + case .howToUseGertrude: return 120 + case .finish: return 125 + } + } +} diff --git a/macapp/App/Sources/App/Onboarding/OnboardingWindow.swift b/macapp/App/Sources/App/Onboarding/OnboardingWindow.swift new file mode 100644 index 00000000..28a39668 --- /dev/null +++ b/macapp/App/Sources/App/Onboarding/OnboardingWindow.swift @@ -0,0 +1,35 @@ +import AppKit +import Combine +import ComposableArchitecture + +class OnboardingWindow: AppWindow { + typealias Feature = OnboardingFeature + typealias State = Feature.State.View + typealias Action = Feature.Action + typealias WebViewAction = Feature.Action.View + + var title = "Onboarding" + var screen = "Onboarding" + var openPublisher: StorePublisher + var cancellables = Set() + var windowDelegate = AppWindowDelegate() + var viewStore: ViewStore + var window: NSWindow? + var closeWindowAction = Action.closeWindow + var initialSize = NSRect(x: 0, y: 0, width: 900, height: 700) + var minSize = NSSize(width: 800, height: 600) + var showTitleBar = false + + @Dependency(\.mainQueue) var mainQueue + @Dependency(\.app) var appClient + + @MainActor init(store: Store) { + viewStore = ViewStore(store, observe: OnboardingFeature.State.View.init) + openPublisher = viewStore.publisher.windowOpen + bind() + } + + func embed(_ webviewAction: WebViewAction) -> Action { + .webview(webviewAction) + } +} diff --git a/macapp/App/Sources/App/RequestSuspension/RequestSuspensionWindow.swift b/macapp/App/Sources/App/RequestSuspension/RequestSuspensionWindow.swift index a42e95af..75bd66c9 100644 --- a/macapp/App/Sources/App/RequestSuspension/RequestSuspensionWindow.swift +++ b/macapp/App/Sources/App/RequestSuspension/RequestSuspensionWindow.swift @@ -19,6 +19,7 @@ class RequestSuspensionWindow: AppWindow { var closeWindowAction = Action.closeWindow var initialSize = NSRect(x: 0, y: 0, width: 680, height: 360) var minSize = NSSize(width: 600, height: 360) + var showTitleBar = false // above almost everything, but below filter installation system prompt var windowLevel = NSWindow.Level.modalPanel diff --git a/macapp/App/Sources/App/WebViewController.swift b/macapp/App/Sources/App/WebViewController.swift index 988ef9ec..b835d414 100644 --- a/macapp/App/Sources/App/WebViewController.swift +++ b/macapp/App/Sources/App/WebViewController.swift @@ -11,6 +11,7 @@ class WebViewController: var webView: WKWebView! var isReady: CurrentValueSubject = .init(false) var send: (Action) -> Void = { _ in } + var withTitleBar = false @Dependency(\.app) var app @@ -27,7 +28,16 @@ class WebViewController: func loadWebView(screen: String) { let webConfiguration = WKWebViewConfiguration() webConfiguration.setValue(true, forKey: "allowUniversalAccessFromFileURLs") - webView = WKWebView(frame: .zero, configuration: webConfiguration) + if #available(macOS 12.3, *) { + // allow embedded youtube videos to go fullscreen + webConfiguration.preferences.isElementFullscreenEnabled = true + } + + if withTitleBar { + webView = WKWebView(frame: .zero, configuration: webConfiguration) + } else { + webView = NoTitleWebView(frame: .zero, configuration: webConfiguration) + } webView.uiDelegate = self webView.setValue(false, forKey: "drawsBackground") @@ -91,6 +101,10 @@ class WebViewController: } } +class NoTitleWebView: WKWebView { + override var mouseDownCanMoveWindow: Bool { true } +} + typealias WebViewControllerOf = WebViewController< F.Reducer.State, F.Reducer.Action diff --git a/macapp/App/Tests/AppTests/AppReducerTests.swift b/macapp/App/Tests/AppTests/AppReducerTests.swift index ce8f69fa..3a357007 100644 --- a/macapp/App/Tests/AppTests/AppReducerTests.swift +++ b/macapp/App/Tests/AppTests/AppReducerTests.swift @@ -74,7 +74,7 @@ import XExpect let saveState = spy(on: Persistent.State.self, returning: ()) store.deps.storage.savePersistentState = saveState.fn - await store.send(.onboarding(.delegate(.saveCurrentStep(.macosUserAccountType)))) + await store.send(.onboarding(.delegate(.saveForResume(.at(step: .macosUserAccountType))))) await expect(saveState.invocations.value[0].resumeOnboarding) .toEqual(.at(step: .macosUserAccountType)) } diff --git a/macapp/App/Tests/AppTests/OnboardingFeatureTests.swift b/macapp/App/Tests/AppTests/OnboardingFeatureTests.swift index a0a46ee8..85228e7e 100644 --- a/macapp/App/Tests/AppTests/OnboardingFeatureTests.swift +++ b/macapp/App/Tests/AppTests/OnboardingFeatureTests.swift @@ -53,15 +53,13 @@ import XExpect users: [ .init(id: 501, name: "Dad", type: .admin), .init(id: 502, name: "liljimmy", type: .standard), - ], - notificationsSetting: .none + ] ))) { $0.onboarding.users = [ .init(id: 501, name: "Dad", isAdmin: true), .init(id: 502, name: "liljimmy", isAdmin: false), ] $0.onboarding.currentUser = .init(id: 502, name: "liljimmy", isAdmin: false) - $0.onboarding.existingNotificationsSetting = .some(.none) } // next they confirm that they have a gertrude account... @@ -98,7 +96,7 @@ import XExpect store.deps.websocket.receive = { Empty().eraseToAnyPublisher() } // they enter code `123456` and click submit... - await store.send(.onboarding(.webview(.connectChildSubmitted(123_456)))) { + await store.send(.onboarding(.webview(.connectChildSubmitted(code: 123_456)))) { $0.onboarding.step = .connectChild $0.onboarding.connectChildRequest = .ongoing // ... and see a throbber } @@ -111,6 +109,7 @@ import XExpect await store.receive(.onboarding(.connectUser(.success(user)))) { $0.user.data = user + $0.history.userConnection = .established(welcomeDismissed: true) $0.onboarding.step = .connectChild $0.onboarding.connectChildRequest = .succeeded(payload: "lil suzy") } @@ -125,9 +124,16 @@ import XExpect await expect(saveState.invocations.value).toHaveCount(3) await expect(saveState.invocations.value[1].user).toEqual(user) + // notifications not enabled + let notifsSettings = mock(returning: [.none], then: NotificationsSetting.alert) + store.deps.device.notificationsSetting = notifsSettings.fn + // they click "next" on the connected child success screen... - await store.send(.onboarding(.webview(.primaryBtnClicked))) { - $0.onboarding.step = .allowNotifications_start // ...and go to notifications screen + await store.send(.onboarding(.webview(.primaryBtnClicked))) + + // ... and end up on the notifications screen + await store.receive(.onboarding(.setStep(.allowNotifications_start))) { + $0.onboarding.step = .allowNotifications_start } let requestNotifAuth = mock(always: ()) @@ -144,27 +150,24 @@ import XExpect await expect(requestNotifAuth.invocations).toEqual(1) await expect(openSysPrefs.invocations).toEqual([.notifications]) - // they did indeed enable notifications... - let notifsSettings = mock(always: NotificationsSetting.alert) - store.deps.device.notificationsSetting = notifsSettings.fn + // they have not previously granted permission... + let screenshotsAllowed = mock(returning: [false, false], then: true) + store.deps.monitoring.screenRecordingPermissionGranted = screenshotsAllowed.fn + // ... and then clicked "Done" on the notifications grant screen await store.send(.onboarding(.webview(.primaryBtnClicked))) // ...and we confirmed the setting and moved them on the happy path - await expect(notifsSettings.invocations).toEqual(1) + await expect(notifsSettings.invocations).toEqual(2) await store.receive(.onboarding(.setStep(.allowScreenshots_required))) { $0.onboarding.step = .allowScreenshots_required } - // they have not previously granted permission... - let screenshotsAllowed = mock(returning: [false], then: true) - store.deps.monitoring.screenRecordingPermissionGranted = screenshotsAllowed.fn - // they click "Grant Permission" on the allow screenshots start screen await store.send(.onboarding(.webview(.primaryBtnClicked))) // ...and we check the setting (which pops up prompt) and moved them on - await expect(screenshotsAllowed.invocations).toEqual(1) + await expect(screenshotsAllowed.invocations).toEqual(2) await store.receive(.onboarding(.setStep(.allowScreenshots_openSysSettings))) { $0.onboarding.step = .allowScreenshots_openSysSettings // ...and go to open } @@ -174,6 +177,9 @@ import XExpect $0.onboarding.step = .allowScreenshots_grantAndRestart // ...and go to grant } + // we record to restart checking screen recording permission... + await store.receive(.onboarding(.delegate(.saveForResume(.checkingScreenRecordingPermission)))) + // NB: here technically they RESTART the app, but instead of starting a new test // we simulate receiving the resume action to carry on where they should // we have other tests testing the resume from persisted state flow. @@ -183,20 +189,21 @@ import XExpect $0.onboarding.step = .allowScreenshots_success } - // they click the "Next" button from the screen recording success - await store.send(.onboarding(.webview(.primaryBtnClicked))) { - $0.onboarding.step = .allowKeylogging_required // ...and go to keylogging - } - // they have not previously granted permission... - let keyloggingAllowed = mock(returning: [false], then: true) + let keyloggingAllowed = mock(returning: [false, false], then: true) store.deps.monitoring.keystrokeRecordingPermissionGranted = keyloggingAllowed.fn + // they click the "Next" button from the screen recording success + await store.send(.onboarding(.webview(.primaryBtnClicked))) + await store.receive(.onboarding(.setStep(.allowKeylogging_required))) { + $0.onboarding.step = .allowKeylogging_required + } + // they click "Grant Permission" on the allow keylogging start screen await store.send(.onboarding(.webview(.primaryBtnClicked))) // ...and we check the setting (which pops up prompt) and moved them on - await expect(keyloggingAllowed.invocations).toEqual(1) + await expect(keyloggingAllowed.invocations).toEqual(2) await store.receive(.onboarding(.setStep(.allowKeylogging_openSysSettings))) { $0.onboarding.step = .allowKeylogging_openSysSettings // ...and go to open } @@ -206,18 +213,24 @@ import XExpect $0.onboarding.step = .allowKeylogging_grant // ...and go to grant } + // moving on from keylogging tests filter extension state, to possibly ski + let filterState = mock(returning: [ + FilterExtensionState.notInstalled, + .notInstalled, + .installedAndRunning, + ]) + store.deps.filterExtension.state = filterState.fn + // they click "Done" indicating they think they've allowed keylogging await store.send(.onboarding(.webview(.primaryBtnClicked))) - // we confirm, and see that the did it correct... - await expect(keyloggingAllowed.invocations).toEqual(2) + // we confirm, and see that they did it correct... + await expect(keyloggingAllowed.invocations).toEqual(3) // ...so they get sent off to the next happy path step await store.receive(.onboarding(.setStep(.installSysExt_explain))) { $0.onboarding.step = .installSysExt_explain // ...and go to sys ext start } - let filterState = mock(returning: [FilterExtensionState.notInstalled, .installedAndRunning]) - store.deps.filterExtension.state = filterState.fn let installSysExt = mock(once: FilterInstallResult.installedSuccessfully) store.deps.filterExtension.install = installSysExt.fn @@ -268,6 +281,123 @@ import XExpect await store.send(.application(.willTerminate)) } + func testSkippingFromAdminUserRemediation() async { + let store = featureStore { + $0.step = .macosUserAccountType + $0.userRemediationStep = .create + } + await store.send(.webview(.secondaryBtnClicked)) { + $0.step = .getChildConnectionCode + } + } + + func testPrimaryBtnFromAllowScreenshotsGrantModalGoesToFailForVideo() async { + let store = featureStore { $0.step = .allowScreenshots_grantAndRestart } + await store.send(.webview(.primaryBtnClicked)) { + $0.step = .allowScreenshots_failed + } + } + + func testSecondaryEscapeHatchFromAllowScreenshotsGrantGoesToNextStage() async { + let store = featureStore { $0.step = .allowScreenshots_grantAndRestart } + store.deps.monitoring.keystrokeRecordingPermissionGranted = { false } + await store.send(.webview(.secondaryBtnClicked)) + await store.receive(.setStep(.allowKeylogging_required)) + } + + func testSecondaryFromAllowNotificationsGrantModalGoesToFail() async { + let store = featureStore { $0.step = .allowNotifications_grant } + await store.send(.webview(.secondaryBtnClicked)) { + $0.step = .allowNotifications_failed + } + } + + func testSkippingAllStepsFromConnectSuccess() async { + let store = featureStore { + $0.step = .connectChild + $0.connectChildRequest = .succeeded(payload: "Lil jimmy") + } + + store.deps.device.notificationsSetting = { .alert } + store.deps.monitoring.screenRecordingPermissionGranted = { true } + store.deps.monitoring.keystrokeRecordingPermissionGranted = { true } + store.deps.filterExtension.state = { .installedAndRunning } + + await store.send(.webview(.primaryBtnClicked)) + await store.receive(.setStep(.locateMenuBarIcon)) + } + + func testSkippingNotificationStepFromConnectSuccess() async { + let store = featureStore { + $0.step = .connectChild + $0.connectChildRequest = .succeeded(payload: "Lil jimmy") + } + + store.deps.device.notificationsSetting = { .alert } + store.deps.monitoring.screenRecordingPermissionGranted = { false } + store.deps.monitoring.keystrokeRecordingPermissionGranted = { fatalError() } + store.deps.filterExtension.state = { fatalError() } + + await store.send(.webview(.primaryBtnClicked)) + await store.receive(.setStep(.allowScreenshots_required)) + } + + func testSkippingToKeyloggingFromConnectSuccess() async { + let store = featureStore { + $0.step = .connectChild + $0.connectChildRequest = .succeeded(payload: "Lil jimmy") + } + + store.deps.device.notificationsSetting = { .alert } + store.deps.monitoring.screenRecordingPermissionGranted = { true } + store.deps.monitoring.keystrokeRecordingPermissionGranted = { false } + store.deps.filterExtension.state = { fatalError() } + + await store.send(.webview(.primaryBtnClicked)) + await store.receive(.setStep(.allowKeylogging_required)) + } + + func testSkippingToInstallSysExtFromConnectSuccess() async { + let store = featureStore { + $0.step = .connectChild + $0.connectChildRequest = .succeeded(payload: "Lil jimmy") + } + + store.deps.device.notificationsSetting = { .alert } + store.deps.monitoring.screenRecordingPermissionGranted = { true } + store.deps.monitoring.keystrokeRecordingPermissionGranted = { true } + store.deps.filterExtension.state = { .notInstalled } + + await store.send(.webview(.primaryBtnClicked)) + await store.receive(.setStep(.installSysExt_explain)) + } + + func testSkippingScreenshotsFromFinishNotifications() async { + let store = featureStore { $0.step = .allowNotifications_grant } + store.deps.monitoring.screenRecordingPermissionGranted = { true } // <-- skip + store.deps.monitoring.keystrokeRecordingPermissionGranted = { false } + store.deps.device.notificationsSetting = { .alert } + await store.send(.webview(.primaryBtnClicked)) + await store.receive(.setStep(.allowKeylogging_required)) + } + + func testSkippingKeyloggingFromFinishScreenshots() async { + let store = featureStore { $0.step = .allowScreenshots_success } + store.deps.monitoring.keystrokeRecordingPermissionGranted = { true } + store.deps.filterExtension.state = { .notInstalled } + await store.send(.webview(.primaryBtnClicked)) + await store.receive(.setStep(.installSysExt_explain)) + } + + func testFromScreenshotsRequiredScreenshotsAndKeyloggingAlreadyAllowed() async { + let store = featureStore { $0.step = .allowScreenshots_required } + store.deps.monitoring.screenRecordingPermissionGranted = { true } + store.deps.monitoring.keystrokeRecordingPermissionGranted = { true } + store.deps.filterExtension.state = { .notInstalled } + await store.send(.webview(.primaryBtnClicked)) + await store.receive(.setStep(.installSysExt_explain)) + } + func testClickingTryAgainPrimaryFromInstallSysExtFailed() async { let store = featureStore { $0.step = .installSysExt_failed } await store.send(.webview(.primaryBtnClicked)) { @@ -284,9 +414,16 @@ import XExpect func testClickingHelpSecondaryFromInstallSysExt() async { let store = featureStore { $0.step = .installSysExt_allow } - await store.send(.webview(.secondaryBtnClicked)) { - $0.step = .installSysExt_failed - } + store.deps.filterExtension.state = { .notInstalled } // <-- not installed + await store.send(.webview(.secondaryBtnClicked)) + await store.receive(.setStep(.installSysExt_failed)) // <-- goes to failed + } + + func testClickingHelpSecondaryFromInstallSysExt_WhenInstalled() async { + let store = featureStore { $0.step = .installSysExt_allow } + store.deps.filterExtension.state = { .installedAndRunning } // <-- installed + await store.send(.webview(.secondaryBtnClicked)) + await store.receive(.setStep(.installSysExt_success)) // <-- goes to success } // for most users, we will move them along automatically to @@ -365,9 +502,16 @@ import XExpect func testSkipAllowKeylogging() async { let store = featureStore { $0.step = .allowKeylogging_required } - await store.send(.webview(.secondaryBtnClicked)) { - $0.step = .installSysExt_explain - } + store.deps.filterExtension.state = { .notInstalled } + await store.send(.webview(.secondaryBtnClicked)) + await store.receive(.setStep(.installSysExt_explain)) + } + + func testSkipAllowKeyloggingSysExtAlreadyInstalled() async { + let store = featureStore { $0.step = .allowKeylogging_required } + store.deps.filterExtension.state = { .installedAndRunning } + await store.send(.webview(.secondaryBtnClicked)) + await store.receive(.setStep(.locateMenuBarIcon)) } func testFailedToAllowKeylogging() async { @@ -387,36 +531,41 @@ import XExpect store.deps.device.openSystemPrefs = openSysPrefs.fn // now they click the "try again" button - await store.send(.webview(.primaryBtnClicked)) { - $0.step = .allowKeylogging_grant - } + await store.send(.webview(.primaryBtnClicked)) + await store.receive(.setStep(.allowKeylogging_grant)) // and we tried to open system prefs to the right spot await expect(openSysPrefs.invocations).toEqual([.security(.accessibility)]) } + func testSkipFromKeylogginFail() async { + let store = featureStore { $0.step = .allowKeylogging_failed } + store.deps.filterExtension.state = { .notInstalled } + await store.send(.webview(.secondaryBtnClicked)) + await store.receive(.setStep(.installSysExt_explain)) + } + func testSkipsMostKeyloggingStepsIfPermsPreviouslyGranted() async { let store = featureStore { $0.step = .allowKeylogging_required } let keyloggingAllowed = mock(always: true) // <- they have granted permission store.deps.monitoring.keystrokeRecordingPermissionGranted = keyloggingAllowed.fn + store.deps.filterExtension.state = { .notInstalled } // they click "Grant permission" on the allow screenshots required screen await store.send(.webview(.primaryBtnClicked)) // ...and we check the setting (which pops up prompt) and moved them on await expect(keyloggingAllowed.invocations).toEqual(1) - await store.receive(.setStep(.installSysExt_explain)) { - $0.step = .installSysExt_explain // ...and go to install system extension - } + await store.receive(.setStep(.installSysExt_explain)) } func testSkipAllowingScreenshots() async { let store = featureStore { $0.step = .allowScreenshots_required } + store.deps.monitoring.keystrokeRecordingPermissionGranted = { false } // they click "Skip" on the allow screenshots start screen - await store.send(.webview(.secondaryBtnClicked)) { - $0.step = .allowKeylogging_required // ...and go to keylogging - } + await store.send(.webview(.secondaryBtnClicked)) + await store.receive(.setStep(.allowKeylogging_required)) } func testSkipsMostScreenshotStepsIfPermsPreviouslyGranted() async { @@ -424,6 +573,7 @@ import XExpect let screenshotsAllowed = mock(always: true) // <- they have granted permission store.deps.monitoring.screenRecordingPermissionGranted = screenshotsAllowed.fn + store.deps.monitoring.keystrokeRecordingPermissionGranted = { false } // they click "Grant permission" on the allow screenshots required screen await store.send(.webview(.primaryBtnClicked)) @@ -436,9 +586,7 @@ import XExpect } func testFailureToGrantNotificationsSendsToFailScreen() async { - let store = featureStore { - $0.step = .allowNotifications_grant - } + let store = featureStore { $0.step = .allowNotifications_grant } let notifsSettings = mock( returning: [NotificationsSetting.none], // <- they did NOT enable notifications... @@ -451,49 +599,31 @@ import XExpect // ...and we fail to confirm the setting, moving them to fail screen await expect(notifsSettings.invocations).toEqual(1) - await store.receive(.setStep(.allowNotifications_failed)) { - $0.step = .allowNotifications_failed - } + await store.receive(.setStep(.allowNotifications_failed)) + + // used to determine if screen recording stage should be skipped + store.deps.monitoring.screenRecordingPermissionGranted = { false } // they fixed it, and clicked Try Again... await store.send(.webview(.primaryBtnClicked)) // ...and we confirmed the setting and moved them on the happy path await expect(notifsSettings.invocations).toEqual(2) - await store.receive(.setStep(.allowScreenshots_required)) { - $0.step = .allowScreenshots_required - } + await store.receive(.setStep(.allowScreenshots_required)) } func testSkipFromAllowNotificationsFailedStep() async { - let store = featureStore { - $0.step = .allowNotifications_failed - } - await store.send(.webview(.secondaryBtnClicked)) { - $0.step = .allowScreenshots_required - } + let store = featureStore { $0.step = .allowNotifications_failed } + store.deps.monitoring.screenRecordingPermissionGranted = { false } + await store.send(.webview(.secondaryBtnClicked)) + await store.receive(.setStep(.allowScreenshots_required)) } func testSkipAllowNotificationsStep() async { - let store = featureStore { - $0.step = .allowNotifications_start - } - await store.send(.webview(.secondaryBtnClicked)) { - $0.step = .allowScreenshots_required - } - } - - func testNotificationsStepSkippedIfAlreadyGranted() async { - let store = featureStore { - $0.step = .connectChild - $0.connectChildRequest = .succeeded(payload: "lil suzy") - $0.existingNotificationsSetting = .alert // <-- already granted - } - - // from the connect child success, they click next... - await store.send(.webview(.primaryBtnClicked)) { - $0.step = .allowScreenshots_required // ...and skip straight to screenshots - } + let store = featureStore { $0.step = .allowNotifications_start } + store.deps.monitoring.screenRecordingPermissionGranted = { false } + await store.send(.webview(.secondaryBtnClicked)) + await store.receive(.setStep(.allowScreenshots_required)) } func testConnectChildFailure() async { @@ -593,21 +723,66 @@ import XExpect store.deps.device.openSystemPrefs = openSysPrefs.fn // they now click the primary "try again" button - await store.send(.onboarding(.webview(.primaryBtnClicked))) { - $0.onboarding.step = .allowScreenshots_grantAndRestart // ... and go back to the grant step - } + await store.send(.onboarding(.webview(.primaryBtnClicked))) + await store.receive(.onboarding(.setStep(.allowScreenshots_grantAndRestart))) // and we tried to open system prefs to the right spot await expect(openSysPrefs.invocations).toEqual([.security(.screenRecording)]) } + func testTryAgainFromScreenRecFailMovesOnIfPermsGranted() async { + let store = featureStore { $0.step = .allowScreenshots_failed } + store.deps.monitoring.screenRecordingPermissionGranted = { true } + await store.send(.webview(.primaryBtnClicked)) + await store.receive(.setStep(.allowScreenshots_success)) + } + func testSkipFromScreenRecordingFailed() async { - let store = featureStore { - $0.step = .allowScreenshots_failed + let store = featureStore { $0.step = .allowScreenshots_failed } + store.deps.monitoring.keystrokeRecordingPermissionGranted = { false } + await store.send(.webview(.secondaryBtnClicked)) + await store.receive(.setStep(.allowKeylogging_required)) + } + + func testNoGertrudeAccountPrimary() async { + let store = featureStore { $0.step = .noGertrudeAccount } + await store.send(.webview(.primaryBtnClicked)) { + $0.step = .macosUserAccountType } + } + + func testSecondaryHelpFromAllowKeyloggingGrantGoesToFailForVideo() async { + let store = featureStore { $0.step = .allowKeylogging_grant } + store.deps.monitoring.keystrokeRecordingPermissionGranted = { false } + await store.send(.webview(.secondaryBtnClicked)) + await store.receive(.setStep(.allowKeylogging_failed)) + } + + func testSecondaryHelpFromAllowKeyloggingGrantGoesToNextIfPermGranted() async { + let store = featureStore { $0.step = .allowKeylogging_grant } + store.deps.monitoring.keystrokeRecordingPermissionGranted = { true } // <-- granted + store.deps.filterExtension.state = { .notInstalled } + await store.send(.webview(.secondaryBtnClicked)) + await store.receive(.setStep(.installSysExt_explain)) + } + + func testSecondaryFromAllowKeyloggingOpenSysSettings() async { + let store = featureStore { $0.step = .allowKeylogging_openSysSettings } + let openSysPrefs = spy(on: SystemPrefsLocation.self, returning: ()) + store.deps.device.openSystemPrefs = openSysPrefs.fn + } + + func testGetHelpClickedFromAllowScreenshotsOpenSysSettings() async { + let store = featureStore { $0.step = .allowScreenshots_openSysSettings } + let openSysPrefs = spy(on: SystemPrefsLocation.self, returning: ()) + store.deps.device.openSystemPrefs = openSysPrefs.fn + await store.send(.webview(.secondaryBtnClicked)) { - $0.step = .allowKeylogging_required + $0.step = .allowScreenshots_grantAndRestart } + + // and we tried to open system prefs to the right spot + await expect(openSysPrefs.invocations).toEqual([.security(.screenRecording)]) } func testNoGertrudeAccountQuit() async { @@ -701,7 +876,7 @@ import XExpect } // remediations require restarting gertrude, so note the step to restart w/ - await store.receive(.delegate(.saveCurrentStep(.macosUserAccountType))) + await store.receive(.delegate(.saveForResume(.at(step: .macosUserAccountType)))) await store.send(.webview(.chooseDemoteAdminClicked)) { $0.userRemediationStep = .demote diff --git a/macapp/App/Tests/Codegen/AppTypeScriptEnums.swift b/macapp/App/Tests/Codegen/AppTypeScriptEnums.swift index ee615fa1..680335c8 100644 --- a/macapp/App/Tests/Codegen/AppTypeScriptEnums.swift +++ b/macapp/App/Tests/Codegen/AppTypeScriptEnums.swift @@ -33,6 +33,12 @@ struct AppTypeScriptEnums: AggregateCodeGenerator { AdminWindowFeature.Action.View.self, ] ), + EnumCodableGen.EnumsGenerator( + name: "OnboardingFeature", + types: [ + OnboardingFeature.Action.View.self, + ] + ), ] func format() throws { diff --git a/macapp/App/Tests/Codegen/AppWebViews.swift b/macapp/App/Tests/Codegen/AppWebViews.swift index 51d91795..77330590 100644 --- a/macapp/App/Tests/Codegen/AppWebViews.swift +++ b/macapp/App/Tests/Codegen/AppWebViews.swift @@ -77,6 +77,26 @@ struct AppWebViews: AggregateCodeGenerator { (AdminAccountStatus.self, "AdminAccountStatus"), ] ), + AppviewStore( + at: "Onboarding/onboarding-store.ts", + namedTypes: [ + .init(OnboardingFeature.State.Step.self, as: "OnboardingStep"), + .init(MacOSVersion.DocumentationGroup.self, as: "OSGroup"), + .init(OnboardingFeature.State.MacUser.RemediationStep.self, as: "UserRemediationStep"), + .init(OnboardingFeature.State.MacUser.self, as: "MacOSUser"), + ], + types: [ + .init(OnboardingFeature.State.View.self, as: "AppState"), + .init(OnboardingFeature.Action.View.self, as: "AppEvent"), + ], + localAliases: [ + (OnboardingFeature.State.Step.self, "OnboardingStep"), + (MacOSVersion.DocumentationGroup.self, "OSGroup"), + (OnboardingFeature.State.MacUser.RemediationStep.self, "UserRemediationStep"), + (OnboardingFeature.State.MacUser.self, "MacOSUser"), + (PayloadRequestState.self, "RequestState"), + ] + ), ] func format() throws { diff --git a/macapp/App/Tests/Codegen/AppviewStore.swift b/macapp/App/Tests/Codegen/AppviewStore.swift index dad3d408..822bad3d 100644 --- a/macapp/App/Tests/Codegen/AppviewStore.swift +++ b/macapp/App/Tests/Codegen/AppviewStore.swift @@ -65,6 +65,9 @@ extension AppviewStore: CodeGenerator { let url = URL(fileURLWithPath: "/Users/jared/gertie/web/appviews/src/\(path)") let file = String(data: try Data(contentsOf: url), encoding: .utf8)! let lines = file.components(separatedBy: "\n") + guard lines.contains("// begin codegen"), lines.contains("// end codegen") else { + fatalError("codegen markers not found in \(path)") + } var updated: [String] = [] var inCodegen = false diff --git a/macapp/Xcode/Gertrude/WebViews/Administrate/index.js b/macapp/Xcode/Gertrude/WebViews/Administrate/index.js index 478dee82..267444db 100644 --- a/macapp/Xcode/Gertrude/WebViews/Administrate/index.js +++ b/macapp/Xcode/Gertrude/WebViews/Administrate/index.js @@ -68,7 +68,7 @@ Error generating stack: `+i.message+` * LICENSE.md file in the root directory of this source tree. * * @license MIT - */function Io(){return Io=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&(n[l]=e[l]);return n}function A0(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function U0(e,t){return e.button===0&&(!t||t==="_self")&&!A0(e)}const b0=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset"],V0=y.forwardRef(function(t,n){let{onClick:r,relative:l,reloadDocument:i,replace:o,state:u,target:s,to:a,preventScrollReset:p}=t,f=j0(t,b0),m=D0(a,{relative:l}),g=H0(a,{replace:o,state:u,target:s,preventScrollReset:p,relative:l});function v(x){r&&r(x),x.defaultPrevented||g(x)}return y.createElement("a",Io({},f,{href:m,onClick:i?r:v,ref:n,target:s}))});var ea;(function(e){e.UseScrollRestoration="useScrollRestoration",e.UseSubmitImpl="useSubmitImpl",e.UseFetcher="useFetcher"})(ea||(ea={}));var ta;(function(e){e.UseFetchers="useFetchers",e.UseScrollRestoration="useScrollRestoration"})(ta||(ta={}));function H0(e,t){let{target:n,replace:r,state:l,preventScrollReset:i,relative:o}=t===void 0?{}:t,u=M0(),s=zu(),a=Ed(e,{relative:o});return y.useCallback(p=>{if(U0(p,n)){p.preventDefault();let f=r!==void 0?r:Ys(s)===Ys(a);u(e,{replace:f,state:l,preventScrollReset:i,relative:o})}},[s,u,a,r,l,n,e,i,o])}const q=({size:e="medium",fullWidth:t=!1,testId:n,color:r,className:l,disabled:i=!1,...o})=>{let u="";if(i)u="bg-slate-50 dark:bg-black/50 text-slate-400 dark:text-slate-600 border border-slate-200 dark:border-slate-800 cursor-not-allowed ring-transparent focus:ring-slate-200";else switch(r){case"primary-on-violet-bg":u="bg-white text-violet-500 hover:bg-violet-50 border-2 border-white hover:border-violet-50 ring-violet-500 ring-offset-violet-500 focus:ring-white";break;case"secondary-on-violet-bg":u="bg-violet-500 text-white border-2 border-white hover:bg-violet-400 ring-violet-500 focus:ring-white ring-offset-violet-500";break;case"primary":u="bg-violet-700 dark:bg-violet-700 border border-violet-700 dark:border-violet-700 hover:border-violet-800 dark:hover:border-violet-700 text-white dark:hover:bg-violet-700 hover:bg-violet-800 ring-transparent focus:ring-violet-700 dark:ring-offset-slate-900";break;case"secondary":u="bg-violet-100 dark:bg-slate-800/80 border border-violet-100 dark:border-slate-700/70 hover:border-violet-200 dark:hover:bg-slate-700/80 text-violet-600 dark:text-white/80 hover:bg-violet-200 ring-transparent focus:ring-violet-300 dark:focus:ring-indigo-500/70 dark:ring-offset-slate-900";break;case"tertiary":u="bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 ring-transparent focus:ring-indigo-400/50 focus:border-indigo-200 dark:ring-offset-slate-900";break;case"warning":u="bg-red-50 dark:bg-red-500/10 text-red-600 dark:text-red-300 border-red-100 dark:border-red-600/50 border hover:text-red-700 hover:bg-red-100 dark:hover:bg-red-500/20 dark:hover:text-red-200 ring-transparent focus:ring-red-500 focus:border-red-500 dark:ring-offset-slate-900";break}const s=o.type==="button"||o.type==="submit";let a="";switch(e){case"small":a="text-base px-4 py-1.5 font-semibold";break;case"medium":a="text-base px-5 py-3 sm:py-2.5";break;default:a="text-lg px-10 py-2.5";break}const p=W(u,"ring ring-offset-0 focus:ring-offset-2 rounded-xl font-bold transition duration-100 outline-none block",a,t?"w-full":"w-auto",!s&&"text-center",l);return s?k("button",{type:o.type,className:p,disabled:i,...n?{"data-test":n}:{},...o.type==="button"?{onClick:i?()=>{}:()=>o.onClick()}:{},children:o.children}):o.type==="external"?k("a",{className:p,...n?{"data-test":n}:{},...i?{onClick:f=>f.preventDefault()}:{href:o.href},children:o.children}):k(V0,{className:p,to:i?"#":o.to,...n?{"data-test":n}:{},onClick:i?f=>f.preventDefault():()=>{},children:o.children})},Cd=({children:e,htmlFor:t,className:n})=>k("label",{htmlFor:t,className:W("text-left text-slate-500 dark:text-slate-300 font-semibold text-md mb-1",n),children:e});function B0(e,t){return t===1?e:`${e}s`}function W0(e){return Q0(new Date(e),"dateInput")}function Q0(e,t){return t==="short"?e.toLocaleDateString():t==="url"?[`${e.getMonth()+1}`.padStart(2,"0"),`${e.getDate()}`.padStart(2,"0"),`${e.getFullYear()}`].join("-"):t==="dateInput"?[`${e.getFullYear()}`,`${e.getMonth()+1}`.padStart(2,"0"),`${e.getDate()}`.padStart(2,"0")].join("-"):[e.toLocaleDateString("en-US",{weekday:"long"}),", ",e.toLocaleDateString("en-US",{month:t==="long"?"long":"short"})," ",e.getDate(),", ",e.getFullYear()].join("")}const _i=({label:e,optional:t,value:n,setValue:r,required:l=!1,autoFocus:i=!1,placeholder:o,className:u,disabled:s,name:a,testId:p,...f})=>{const[m,g]=y.useState(n),v=y.useId(),x=bt(f)?"input":"textarea";return L("div",{className:W("flex flex-col space-y-1 w-full",u),children:[(e||t)&&L("div",{className:"flex flex-row justify-between items-center",children:[e&&k(Cd,{htmlFor:v,children:e}),t&&k("span",{className:"text-violet-500/80 translate-y-px text-sm antialiased italic",children:"*optional"})]}),L("div",{className:"flex shadow-sm rounded-lg",children:[bt(f)&&f.prefix&&k("div",{className:"hidden xs:flex justify-center items-center p-3 bg-slate-50 dark:bg-slate-700/50 border border-r-0 dark:border-slate-700 rounded-l-lg",children:k("h3",{className:"text-slate-500 dark:text-slate-400",children:f.prefix})}),k(x,{id:v,type:f.type==="positiveInteger"?"number":f.type,value:m,required:!!l,autoFocus:i,placeholder:o,disabled:s,name:a,...p?{"data-test":p}:{},...f.type==="url"?{autoCapitalize:"none",autoCorrect:"off"}:{},...f.type==="password"?{minLength:4}:{},...f.type==="date"?{min:W0(new Date().toISOString())}:{},...bt(f)?{}:{rows:f.rows},onChange:N=>{const d=N.target.value;g(d),(f.type!=="positiveInteger"||K0(d))&&r(d)},className:W("py-3 px-4 flex-grow w-12","border border-slate-200 rounded-lg","transition-[border-color,ring-color] duration-150","text-slate-600 placeholder:text-slate-400/90 placeholder:antialiased","ring-0 ring-slate-200 outline-none focus:shadow-md focus:border-indigo-500 focus:ring-indigo-500 focus:ring-1","dark:bg-slate-700/20 dark:border-slate-700 dark:placeholder:text-slate-500 dark:text-white",!bt(f)&&f.noResize&&"resize-none",bt(f)&&f.unit&&"rounded-r-none",bt(f)&&f.prefix&&"xs:rounded-l-none")}),bt(f)&&f.unit&&k("div",{className:"flex justify-center items-center p-3 bg-slate-50 dark:bg-slate-700/50 border border-l-0 dark:border-slate-700 rounded-r-lg",children:k("h3",{className:"text-slate-500 dark:text-slate-400",children:f.unit})})]})]})};function K0(e){return e.match(/^[0-9]+$/)!==null&&Number.isInteger(Number(e))&&Number(e)>=0}function bt(e){return e.type!=="textarea"}var G0=Object.defineProperty,Y0=(e,t,n)=>t in e?G0(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,Li=(e,t,n)=>(Y0(e,typeof t!="symbol"?t+"":t,n),n);let X0=class{constructor(){Li(this,"current",this.detect()),Li(this,"handoffState","pending"),Li(this,"currentId",0)}set(t){this.current!==t&&(this.handoffState="pending",this.currentId=0,this.current=t)}reset(){this.set(this.detect())}nextId(){return++this.currentId}get isServer(){return this.current==="server"}get isClient(){return this.current==="client"}detect(){return typeof window>"u"||typeof document>"u"?"server":"client"}handoff(){this.handoffState==="pending"&&(this.handoffState="complete")}get isHandoffComplete(){return this.handoffState==="complete"}},zt=new X0,be=(e,t)=>{zt.isServer?y.useEffect(e,t):y.useLayoutEffect(e,t)};function ct(e){let t=y.useRef(e);return be(()=>{t.current=e},[e]),t}function Nd(e,t){let[n,r]=y.useState(e),l=ct(e);return be(()=>r(l.current),[l,r,...t]),n}function Z0(e){typeof queueMicrotask=="function"?queueMicrotask(e):Promise.resolve().then(e).catch(t=>setTimeout(()=>{throw t}))}function tn(){let e=[],t=[],n={enqueue(r){t.push(r)},addEventListener(r,l,i,o){return r.addEventListener(l,i,o),n.add(()=>r.removeEventListener(l,i,o))},requestAnimationFrame(...r){let l=requestAnimationFrame(...r);return n.add(()=>cancelAnimationFrame(l))},nextFrame(...r){return n.requestAnimationFrame(()=>n.requestAnimationFrame(...r))},setTimeout(...r){let l=setTimeout(...r);return n.add(()=>clearTimeout(l))},microTask(...r){let l={current:!0};return Z0(()=>{l.current&&r[0]()}),n.add(()=>{l.current=!1})},add(r){return e.push(r),()=>{let l=e.indexOf(r);if(l>=0){let[i]=e.splice(l,1);i()}}},dispose(){for(let r of e.splice(0))r()},async workQueue(){for(let r of t.splice(0))await r()},style(r,l,i){let o=r.style.getPropertyValue(l);return Object.assign(r.style,{[l]:i}),this.add(()=>{Object.assign(r.style,{[l]:o})})}};return n}function In(){let[e]=y.useState(tn);return y.useEffect(()=>()=>e.dispose(),[e]),e}let A=function(e){let t=ct(e);return ue.useCallback((...n)=>t.current(...n),[t])};function $u(){let[e,t]=y.useState(zt.isHandoffComplete);return e&&zt.isHandoffComplete===!1&&t(!1),y.useEffect(()=>{e!==!0&&t(!0)},[e]),y.useEffect(()=>zt.handoff(),[]),e}var na;let Xl=(na=ue.useId)!=null?na:function(){let e=$u(),[t,n]=ue.useState(e?()=>zt.nextId():null);return be(()=>{t===null&&n(zt.nextId())},[t]),t!=null?""+t:void 0};function ce(e,t,...n){if(e in t){let l=t[e];return typeof l=="function"?l(...n):l}let r=new Error(`Tried to handle "${e}" but there is no handler defined. Only defined handlers are: ${Object.keys(t).map(l=>`"${l}"`).join(", ")}.`);throw Error.captureStackTrace&&Error.captureStackTrace(r,ce),r}function Pd(e){return zt.isServer?null:e instanceof Node?e.ownerDocument:e!=null&&e.hasOwnProperty("current")&&e.current instanceof Node?e.current.ownerDocument:document}let ra=["[contentEditable=true]","[tabindex]","a[href]","area[href]","button:not([disabled])","iframe","input:not([disabled])","select:not([disabled])","textarea:not([disabled])"].map(e=>`${e}:not([tabindex='-1'])`).join(",");var J0=(e=>(e[e.First=1]="First",e[e.Previous=2]="Previous",e[e.Next=4]="Next",e[e.Last=8]="Last",e[e.WrapAround=16]="WrapAround",e[e.NoScroll=32]="NoScroll",e))(J0||{}),q0=(e=>(e[e.Error=0]="Error",e[e.Overflow=1]="Overflow",e[e.Success=2]="Success",e[e.Underflow=3]="Underflow",e))(q0||{}),eh=(e=>(e[e.Previous=-1]="Previous",e[e.Next=1]="Next",e))(eh||{}),Iu=(e=>(e[e.Strict=0]="Strict",e[e.Loose=1]="Loose",e))(Iu||{});function Od(e,t=0){var n;return e===((n=Pd(e))==null?void 0:n.body)?!1:ce(t,{[0](){return e.matches(ra)},[1](){let r=e;for(;r!==null;){if(r.matches(ra))return!0;r=r.parentElement}return!1}})}function th(e,t=n=>n){return e.slice().sort((n,r)=>{let l=t(n),i=t(r);if(l===null||i===null)return 0;let o=l.compareDocumentPosition(i);return o&Node.DOCUMENT_POSITION_FOLLOWING?-1:o&Node.DOCUMENT_POSITION_PRECEDING?1:0})}function Ti(e,t,n){let r=ct(t);y.useEffect(()=>{function l(i){r.current(i)}return document.addEventListener(e,l,n),()=>document.removeEventListener(e,l,n)},[e,n])}function nh(e,t,n=!0){let r=y.useRef(!1);y.useEffect(()=>{requestAnimationFrame(()=>{r.current=n})},[n]);function l(o,u){if(!r.current||o.defaultPrevented)return;let s=function p(f){return typeof f=="function"?p(f()):Array.isArray(f)||f instanceof Set?f:[f]}(e),a=u(o);if(a!==null&&a.getRootNode().contains(a)){for(let p of s){if(p===null)continue;let f=p instanceof HTMLElement?p:p.current;if(f!=null&&f.contains(a)||o.composed&&o.composedPath().includes(f))return}return!Od(a,Iu.Loose)&&a.tabIndex!==-1&&o.preventDefault(),t(o,a)}}let i=y.useRef(null);Ti("mousedown",o=>{var u,s;r.current&&(i.current=((s=(u=o.composedPath)==null?void 0:u.call(o))==null?void 0:s[0])||o.target)},!0),Ti("click",o=>{!i.current||(l(o,()=>i.current),i.current=null)},!0),Ti("blur",o=>l(o,()=>window.document.activeElement instanceof HTMLIFrameElement?window.document.activeElement:null),!0)}function la(e){var t;if(e.type)return e.type;let n=(t=e.as)!=null?t:"button";if(typeof n=="string"&&n.toLowerCase()==="button")return"button"}function rh(e,t){let[n,r]=y.useState(()=>la(e));return be(()=>{r(la(e))},[e.type,e.as]),be(()=>{n||!t.current||t.current instanceof HTMLButtonElement&&!t.current.hasAttribute("type")&&r("button")},[n,t]),n}let lh=Symbol();function ln(...e){let t=y.useRef(e);y.useEffect(()=>{t.current=e},[e]);let n=A(r=>{for(let l of t.current)l!=null&&(typeof l=="function"?l(r):l.current=r)});return e.every(r=>r==null||r?.[lh])?void 0:n}function ih(e){throw new Error("Unexpected object: "+e)}var Ee=(e=>(e[e.First=0]="First",e[e.Previous=1]="Previous",e[e.Next=2]="Next",e[e.Last=3]="Last",e[e.Specific=4]="Specific",e[e.Nothing=5]="Nothing",e))(Ee||{});function oh(e,t){let n=t.resolveItems();if(n.length<=0)return null;let r=t.resolveActiveIndex(),l=r??-1,i=(()=>{switch(e.focus){case 0:return n.findIndex(o=>!t.resolveDisabled(o));case 1:{let o=n.slice().reverse().findIndex((u,s,a)=>l!==-1&&a.length-s-1>=l?!1:!t.resolveDisabled(u));return o===-1?o:n.length-1-o}case 2:return n.findIndex((o,u)=>u<=l?!1:!t.resolveDisabled(o));case 3:{let o=n.slice().reverse().findIndex(u=>!t.resolveDisabled(u));return o===-1?o:n.length-1-o}case 4:return n.findIndex(o=>t.resolveId(o)===e.id);case 5:return null;default:ih(e)}})();return i===-1?r:i}function _d(...e){return e.filter(Boolean).join(" ")}var $l=(e=>(e[e.None=0]="None",e[e.RenderStrategy=1]="RenderStrategy",e[e.Static=2]="Static",e))($l||{}),st=(e=>(e[e.Unmount=0]="Unmount",e[e.Hidden=1]="Hidden",e))(st||{});function jt({ourProps:e,theirProps:t,slot:n,defaultTag:r,features:l,visible:i=!0,name:o}){let u=Ld(t,e);if(i)return Gr(u,n,r,o);let s=l??0;if(s&2){let{static:a=!1,...p}=u;if(a)return Gr(p,n,r,o)}if(s&1){let{unmount:a=!0,...p}=u;return ce(a?0:1,{[0](){return null},[1](){return Gr({...p,hidden:!0,style:{display:"none"}},n,r,o)}})}return Gr(u,n,r,o)}function Gr(e,t={},n,r){var l;let{as:i=n,children:o,refName:u="ref",...s}=Ri(e,["unmount","static"]),a=e.ref!==void 0?{[u]:e.ref}:{},p=typeof o=="function"?o(t):o;s.className&&typeof s.className=="function"&&(s.className=s.className(t));let f={};if(t){let m=!1,g=[];for(let[v,x]of Object.entries(t))typeof x=="boolean"&&(m=!0),x===!0&&g.push(v);m&&(f["data-headlessui-state"]=g.join(" "))}if(i===y.Fragment&&Object.keys(Fo(s)).length>0){if(!y.isValidElement(p)||Array.isArray(p)&&p.length>1)throw new Error(['Passing props on "Fragment"!',"",`The current component <${r} /> is rendering a "Fragment".`,"However we need to passthrough the following props:",Object.keys(s).map(v=>` - ${v}`).join(` + */function Io(){return Io=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&(n[l]=e[l]);return n}function A0(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function U0(e,t){return e.button===0&&(!t||t==="_self")&&!A0(e)}const b0=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset"],V0=y.forwardRef(function(t,n){let{onClick:r,relative:l,reloadDocument:i,replace:o,state:u,target:s,to:a,preventScrollReset:p}=t,f=j0(t,b0),m=D0(a,{relative:l}),g=H0(a,{replace:o,state:u,target:s,preventScrollReset:p,relative:l});function v(x){r&&r(x),x.defaultPrevented||g(x)}return y.createElement("a",Io({},f,{href:m,onClick:i?r:v,ref:n,target:s}))});var ea;(function(e){e.UseScrollRestoration="useScrollRestoration",e.UseSubmitImpl="useSubmitImpl",e.UseFetcher="useFetcher"})(ea||(ea={}));var ta;(function(e){e.UseFetchers="useFetchers",e.UseScrollRestoration="useScrollRestoration"})(ta||(ta={}));function H0(e,t){let{target:n,replace:r,state:l,preventScrollReset:i,relative:o}=t===void 0?{}:t,u=M0(),s=zu(),a=Ed(e,{relative:o});return y.useCallback(p=>{if(U0(p,n)){p.preventDefault();let f=r!==void 0?r:Ys(s)===Ys(a);u(e,{replace:f,state:l,preventScrollReset:i,relative:o})}},[s,u,a,r,l,n,e,i,o])}const q=({size:e="medium",fullWidth:t=!1,testId:n,color:r,className:l,disabled:i=!1,...o})=>{let u="";if(i)u="bg-slate-50 dark:bg-black/50 text-slate-400 dark:text-slate-600 border border-slate-200 dark:border-slate-800 cursor-not-allowed ring-transparent focus:ring-slate-200";else switch(r){case"primary-on-violet-bg":u="bg-white text-violet-500 hover:bg-violet-50 border-2 border-white hover:border-violet-50 ring-violet-500 ring-offset-violet-500 focus:ring-white";break;case"secondary-on-violet-bg":u="bg-violet-500 text-white border-2 border-white hover:bg-violet-400 ring-violet-500 focus:ring-white ring-offset-violet-500";break;case"primary":u="bg-violet-700 dark:bg-violet-700 border border-violet-700 dark:border-violet-700 hover:border-violet-800 dark:hover:border-violet-700 text-white dark:hover:bg-violet-700 hover:bg-violet-800 ring-transparent focus:ring-violet-700 dark:ring-offset-slate-900";break;case"secondary":u="bg-violet-100 dark:bg-slate-800/80 border border-violet-100 dark:border-slate-700/70 hover:border-violet-200 dark:hover:bg-slate-700/80 text-violet-600 dark:text-white/80 hover:bg-violet-200 ring-transparent focus:ring-violet-300 dark:focus:ring-indigo-500/70 dark:ring-offset-slate-900";break;case"tertiary":u="bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 ring-transparent focus:ring-indigo-400/50 focus:border-indigo-200 dark:ring-offset-slate-900";break;case"warning":u="bg-red-50 dark:bg-red-500/10 text-red-600 dark:text-red-300 border-red-100 dark:border-red-600/50 border hover:text-red-700 hover:bg-red-100 dark:hover:bg-red-500/20 dark:hover:text-red-200 ring-transparent focus:ring-red-500 focus:border-red-500 dark:ring-offset-slate-900";break}const s=o.type==="button"||o.type==="submit";let a="";switch(e){case"small":a="text-base px-4 py-1.5 font-semibold";break;case"medium":a="text-base px-5 py-3 sm:py-2.5";break;default:a="text-lg px-10 py-2.5";break}const p=W(u,"ring ring-offset-0 focus:ring-offset-2 rounded-xl font-bold transition duration-200 outline-none block active:scale-[0.98] leading-[1.25em] select-none",a,t?"w-full":"w-auto",!s&&"text-center",l);return s?k("button",{type:o.type,className:p,disabled:i,...n?{"data-test":n}:{},...o.type==="button"?{onClick:i?()=>{}:()=>o.onClick()}:{},children:o.children}):o.type==="external"?k("a",{className:p,...n?{"data-test":n}:{},...i?{onClick:f=>f.preventDefault()}:{href:o.href},children:o.children}):k(V0,{className:p,to:i?"#":o.to,...n?{"data-test":n}:{},onClick:i?f=>f.preventDefault():()=>{},children:o.children})},Cd=({children:e,htmlFor:t,className:n})=>k("label",{htmlFor:t,className:W("text-left text-slate-700 dark:text-slate-200 font-medium text-md mb-1",n),children:e});function B0(e,t){return t===1?e:`${e}s`}function W0(e){return Q0(new Date(e),"dateInput")}function Q0(e,t){return t==="short"?e.toLocaleDateString():t==="url"?[`${e.getMonth()+1}`.padStart(2,"0"),`${e.getDate()}`.padStart(2,"0"),`${e.getFullYear()}`].join("-"):t==="dateInput"?[`${e.getFullYear()}`,`${e.getMonth()+1}`.padStart(2,"0"),`${e.getDate()}`.padStart(2,"0")].join("-"):[e.toLocaleDateString("en-US",{weekday:"long"}),", ",e.toLocaleDateString("en-US",{month:t==="long"?"long":"short"})," ",e.getDate(),", ",e.getFullYear()].join("")}const _i=({label:e,optional:t,value:n,setValue:r,required:l=!1,autoFocus:i=!1,placeholder:o,className:u,disabled:s,name:a,testId:p,...f})=>{const[m,g]=y.useState(n),v=y.useId(),x=bt(f)?"input":"textarea";return L("div",{className:W("flex flex-col space-y-1 w-full",u),children:[(e||t)&&L("div",{className:"flex flex-row justify-between items-center",children:[e&&k(Cd,{htmlFor:v,children:e}),t&&k("span",{className:"text-violet-500/80 font-medium translate-y-px text-sm antialiased italic",children:"*optional"})]}),L("div",{className:"flex shadow-sm rounded-xl",children:[bt(f)&&f.prefix&&k("div",{className:"hidden xs:flex justify-center items-center p-3 bg-slate-50 dark:bg-slate-700/50 border border-r-0 dark:border-slate-700 rounded-l-xl",children:k("h3",{className:"text-slate-500 dark:text-slate-400",children:f.prefix})}),k(x,{id:v,type:f.type==="positiveInteger"?"number":f.type,value:m,required:!!l,autoFocus:i,placeholder:o,disabled:s,name:a,...p?{"data-test":p}:{},...f.type==="url"?{autoCapitalize:"none",autoCorrect:"off"}:{},...f.type==="password"?{minLength:4}:{},...f.type==="date"?{min:W0(new Date().toISOString())}:{},...bt(f)?{}:{rows:f.rows},onChange:N=>{const d=N.target.value;g(d),(f.type!=="positiveInteger"||K0(d))&&r(d)},className:W("py-3 px-4 flex-grow w-12","border border-slate-200 rounded-xl","transition-[border-color,ring-color] duration-150","text-slate-600 placeholder:text-slate-400/90 placeholder:antialiased","ring-0 ring-slate-200 outline-none focus:shadow-md focus:border-indigo-500 focus:ring-indigo-500 focus:ring-1","dark:bg-slate-700/20 dark:border-slate-700 dark:placeholder:text-slate-500 dark:text-white",!bt(f)&&f.noResize&&"resize-none",bt(f)&&f.unit&&"rounded-r-none",bt(f)&&f.prefix&&"xs:rounded-l-none")}),bt(f)&&f.unit&&k("div",{className:"flex justify-center items-center p-3 bg-slate-50 dark:bg-slate-700/50 border border-l-0 dark:border-slate-700 rounded-r-xl",children:k("h3",{className:"text-slate-500 dark:text-slate-400",children:f.unit})})]})]})};function K0(e){return e.match(/^[0-9]+$/)!==null&&Number.isInteger(Number(e))&&Number(e)>=0}function bt(e){return e.type!=="textarea"}var G0=Object.defineProperty,Y0=(e,t,n)=>t in e?G0(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,Li=(e,t,n)=>(Y0(e,typeof t!="symbol"?t+"":t,n),n);let X0=class{constructor(){Li(this,"current",this.detect()),Li(this,"handoffState","pending"),Li(this,"currentId",0)}set(t){this.current!==t&&(this.handoffState="pending",this.currentId=0,this.current=t)}reset(){this.set(this.detect())}nextId(){return++this.currentId}get isServer(){return this.current==="server"}get isClient(){return this.current==="client"}detect(){return typeof window>"u"||typeof document>"u"?"server":"client"}handoff(){this.handoffState==="pending"&&(this.handoffState="complete")}get isHandoffComplete(){return this.handoffState==="complete"}},zt=new X0,be=(e,t)=>{zt.isServer?y.useEffect(e,t):y.useLayoutEffect(e,t)};function ct(e){let t=y.useRef(e);return be(()=>{t.current=e},[e]),t}function Nd(e,t){let[n,r]=y.useState(e),l=ct(e);return be(()=>r(l.current),[l,r,...t]),n}function Z0(e){typeof queueMicrotask=="function"?queueMicrotask(e):Promise.resolve().then(e).catch(t=>setTimeout(()=>{throw t}))}function tn(){let e=[],t=[],n={enqueue(r){t.push(r)},addEventListener(r,l,i,o){return r.addEventListener(l,i,o),n.add(()=>r.removeEventListener(l,i,o))},requestAnimationFrame(...r){let l=requestAnimationFrame(...r);return n.add(()=>cancelAnimationFrame(l))},nextFrame(...r){return n.requestAnimationFrame(()=>n.requestAnimationFrame(...r))},setTimeout(...r){let l=setTimeout(...r);return n.add(()=>clearTimeout(l))},microTask(...r){let l={current:!0};return Z0(()=>{l.current&&r[0]()}),n.add(()=>{l.current=!1})},add(r){return e.push(r),()=>{let l=e.indexOf(r);if(l>=0){let[i]=e.splice(l,1);i()}}},dispose(){for(let r of e.splice(0))r()},async workQueue(){for(let r of t.splice(0))await r()},style(r,l,i){let o=r.style.getPropertyValue(l);return Object.assign(r.style,{[l]:i}),this.add(()=>{Object.assign(r.style,{[l]:o})})}};return n}function In(){let[e]=y.useState(tn);return y.useEffect(()=>()=>e.dispose(),[e]),e}let A=function(e){let t=ct(e);return ue.useCallback((...n)=>t.current(...n),[t])};function $u(){let[e,t]=y.useState(zt.isHandoffComplete);return e&&zt.isHandoffComplete===!1&&t(!1),y.useEffect(()=>{e!==!0&&t(!0)},[e]),y.useEffect(()=>zt.handoff(),[]),e}var na;let Xl=(na=ue.useId)!=null?na:function(){let e=$u(),[t,n]=ue.useState(e?()=>zt.nextId():null);return be(()=>{t===null&&n(zt.nextId())},[t]),t!=null?""+t:void 0};function ce(e,t,...n){if(e in t){let l=t[e];return typeof l=="function"?l(...n):l}let r=new Error(`Tried to handle "${e}" but there is no handler defined. Only defined handlers are: ${Object.keys(t).map(l=>`"${l}"`).join(", ")}.`);throw Error.captureStackTrace&&Error.captureStackTrace(r,ce),r}function Pd(e){return zt.isServer?null:e instanceof Node?e.ownerDocument:e!=null&&e.hasOwnProperty("current")&&e.current instanceof Node?e.current.ownerDocument:document}let ra=["[contentEditable=true]","[tabindex]","a[href]","area[href]","button:not([disabled])","iframe","input:not([disabled])","select:not([disabled])","textarea:not([disabled])"].map(e=>`${e}:not([tabindex='-1'])`).join(",");var J0=(e=>(e[e.First=1]="First",e[e.Previous=2]="Previous",e[e.Next=4]="Next",e[e.Last=8]="Last",e[e.WrapAround=16]="WrapAround",e[e.NoScroll=32]="NoScroll",e))(J0||{}),q0=(e=>(e[e.Error=0]="Error",e[e.Overflow=1]="Overflow",e[e.Success=2]="Success",e[e.Underflow=3]="Underflow",e))(q0||{}),eh=(e=>(e[e.Previous=-1]="Previous",e[e.Next=1]="Next",e))(eh||{}),Iu=(e=>(e[e.Strict=0]="Strict",e[e.Loose=1]="Loose",e))(Iu||{});function Od(e,t=0){var n;return e===((n=Pd(e))==null?void 0:n.body)?!1:ce(t,{[0](){return e.matches(ra)},[1](){let r=e;for(;r!==null;){if(r.matches(ra))return!0;r=r.parentElement}return!1}})}function th(e,t=n=>n){return e.slice().sort((n,r)=>{let l=t(n),i=t(r);if(l===null||i===null)return 0;let o=l.compareDocumentPosition(i);return o&Node.DOCUMENT_POSITION_FOLLOWING?-1:o&Node.DOCUMENT_POSITION_PRECEDING?1:0})}function Ti(e,t,n){let r=ct(t);y.useEffect(()=>{function l(i){r.current(i)}return document.addEventListener(e,l,n),()=>document.removeEventListener(e,l,n)},[e,n])}function nh(e,t,n=!0){let r=y.useRef(!1);y.useEffect(()=>{requestAnimationFrame(()=>{r.current=n})},[n]);function l(o,u){if(!r.current||o.defaultPrevented)return;let s=function p(f){return typeof f=="function"?p(f()):Array.isArray(f)||f instanceof Set?f:[f]}(e),a=u(o);if(a!==null&&a.getRootNode().contains(a)){for(let p of s){if(p===null)continue;let f=p instanceof HTMLElement?p:p.current;if(f!=null&&f.contains(a)||o.composed&&o.composedPath().includes(f))return}return!Od(a,Iu.Loose)&&a.tabIndex!==-1&&o.preventDefault(),t(o,a)}}let i=y.useRef(null);Ti("mousedown",o=>{var u,s;r.current&&(i.current=((s=(u=o.composedPath)==null?void 0:u.call(o))==null?void 0:s[0])||o.target)},!0),Ti("click",o=>{!i.current||(l(o,()=>i.current),i.current=null)},!0),Ti("blur",o=>l(o,()=>window.document.activeElement instanceof HTMLIFrameElement?window.document.activeElement:null),!0)}function la(e){var t;if(e.type)return e.type;let n=(t=e.as)!=null?t:"button";if(typeof n=="string"&&n.toLowerCase()==="button")return"button"}function rh(e,t){let[n,r]=y.useState(()=>la(e));return be(()=>{r(la(e))},[e.type,e.as]),be(()=>{n||!t.current||t.current instanceof HTMLButtonElement&&!t.current.hasAttribute("type")&&r("button")},[n,t]),n}let lh=Symbol();function ln(...e){let t=y.useRef(e);y.useEffect(()=>{t.current=e},[e]);let n=A(r=>{for(let l of t.current)l!=null&&(typeof l=="function"?l(r):l.current=r)});return e.every(r=>r==null||r?.[lh])?void 0:n}function ih(e){throw new Error("Unexpected object: "+e)}var Ee=(e=>(e[e.First=0]="First",e[e.Previous=1]="Previous",e[e.Next=2]="Next",e[e.Last=3]="Last",e[e.Specific=4]="Specific",e[e.Nothing=5]="Nothing",e))(Ee||{});function oh(e,t){let n=t.resolveItems();if(n.length<=0)return null;let r=t.resolveActiveIndex(),l=r??-1,i=(()=>{switch(e.focus){case 0:return n.findIndex(o=>!t.resolveDisabled(o));case 1:{let o=n.slice().reverse().findIndex((u,s,a)=>l!==-1&&a.length-s-1>=l?!1:!t.resolveDisabled(u));return o===-1?o:n.length-1-o}case 2:return n.findIndex((o,u)=>u<=l?!1:!t.resolveDisabled(o));case 3:{let o=n.slice().reverse().findIndex(u=>!t.resolveDisabled(u));return o===-1?o:n.length-1-o}case 4:return n.findIndex(o=>t.resolveId(o)===e.id);case 5:return null;default:ih(e)}})();return i===-1?r:i}function _d(...e){return e.filter(Boolean).join(" ")}var $l=(e=>(e[e.None=0]="None",e[e.RenderStrategy=1]="RenderStrategy",e[e.Static=2]="Static",e))($l||{}),st=(e=>(e[e.Unmount=0]="Unmount",e[e.Hidden=1]="Hidden",e))(st||{});function jt({ourProps:e,theirProps:t,slot:n,defaultTag:r,features:l,visible:i=!0,name:o}){let u=Ld(t,e);if(i)return Gr(u,n,r,o);let s=l??0;if(s&2){let{static:a=!1,...p}=u;if(a)return Gr(p,n,r,o)}if(s&1){let{unmount:a=!0,...p}=u;return ce(a?0:1,{[0](){return null},[1](){return Gr({...p,hidden:!0,style:{display:"none"}},n,r,o)}})}return Gr(u,n,r,o)}function Gr(e,t={},n,r){var l;let{as:i=n,children:o,refName:u="ref",...s}=Ri(e,["unmount","static"]),a=e.ref!==void 0?{[u]:e.ref}:{},p=typeof o=="function"?o(t):o;s.className&&typeof s.className=="function"&&(s.className=s.className(t));let f={};if(t){let m=!1,g=[];for(let[v,x]of Object.entries(t))typeof x=="boolean"&&(m=!0),x===!0&&g.push(v);m&&(f["data-headlessui-state"]=g.join(" "))}if(i===y.Fragment&&Object.keys(Fo(s)).length>0){if(!y.isValidElement(p)||Array.isArray(p)&&p.length>1)throw new Error(['Passing props on "Fragment"!',"",`The current component <${r} /> is rendering a "Fragment".`,"However we need to passthrough the following props:",Object.keys(s).map(v=>` - ${v}`).join(` `),"","You can apply a few solutions:",['Add an `as="..."` prop, to ensure that we render an actual element instead of a "Fragment".',"Render a single element as the child so that we can forward the props onto that element."].map(v=>` - ${v}`).join(` `)].join(` -`));let m=_d((l=p.props)==null?void 0:l.className,s.className),g=m?{className:m}:{};return y.cloneElement(p,Object.assign({},Ld(p.props,Fo(Ri(s,["ref"]))),f,a,uh(p.ref,a.ref),g))}return y.createElement(i,Object.assign({},Ri(s,["ref"]),i!==y.Fragment&&a,i!==y.Fragment&&f),p)}function uh(...e){return{ref:e.every(t=>t==null)?void 0:t=>{for(let n of e)n!=null&&(typeof n=="function"?n(t):n.current=t)}}}function Ld(...e){if(e.length===0)return{};if(e.length===1)return e[0];let t={},n={};for(let r of e)for(let l in r)l.startsWith("on")&&typeof r[l]=="function"?(n[l]!=null||(n[l]=[]),n[l].push(r[l])):t[l]=r[l];if(t.disabled||t["aria-disabled"])return Object.assign(t,Object.fromEntries(Object.keys(n).map(r=>[r,void 0])));for(let r in n)Object.assign(t,{[r](l,...i){let o=n[r];for(let u of o){if((l instanceof Event||l?.nativeEvent instanceof Event)&&l.defaultPrevented)return;u(l,...i)}}});return t}function vt(e){var t;return Object.assign(y.forwardRef(e),{displayName:(t=e.displayName)!=null?t:e.name})}function Fo(e){let t=Object.assign({},e);for(let n in t)t[n]===void 0&&delete t[n];return t}function Ri(e,t=[]){let n=Object.assign({},e);for(let r of t)r in n&&delete n[r];return n}function sh(e){let t=e.parentElement,n=null;for(;t&&!(t instanceof HTMLFieldSetElement);)t instanceof HTMLLegendElement&&(n=t),t=t.parentElement;let r=t?.getAttribute("disabled")==="";return r&&ah(n)?!1:r}function ah(e){if(!e)return!1;let t=e.previousElementSibling;for(;t!==null;){if(t instanceof HTMLLegendElement)return!1;t=t.previousElementSibling}return!0}function Td(e={},t=null,n=[]){for(let[r,l]of Object.entries(e))zd(n,Rd(t,r),l);return n}function Rd(e,t){return e?e+"["+t+"]":t}function zd(e,t,n){if(Array.isArray(n))for(let[r,l]of n.entries())zd(e,Rd(t,r.toString()),l);else n instanceof Date?e.push([t,n.toISOString()]):typeof n=="boolean"?e.push([t,n?"1":"0"]):typeof n=="string"?e.push([t,n]):typeof n=="number"?e.push([t,`${n}`]):n==null?e.push([t,""]):Td(n,t,e)}let ch="div";var $d=(e=>(e[e.None=1]="None",e[e.Focusable=2]="Focusable",e[e.Hidden=4]="Hidden",e))($d||{});let dh=vt(function(e,t){let{features:n=1,...r}=e,l={ref:t,"aria-hidden":(n&2)===2?!0:void 0,style:{position:"fixed",top:1,left:1,width:1,height:0,padding:0,margin:-1,overflow:"hidden",clip:"rect(0, 0, 0, 0)",whiteSpace:"nowrap",borderWidth:"0",...(n&4)===4&&(n&2)!==2&&{display:"none"}}};return jt({ourProps:l,theirProps:r,slot:{},defaultTag:ch,name:"Hidden"})}),Fu=y.createContext(null);Fu.displayName="OpenClosedContext";var _e=(e=>(e[e.Open=1]="Open",e[e.Closed=2]="Closed",e[e.Closing=4]="Closing",e[e.Opening=8]="Opening",e))(_e||{});function Du(){return y.useContext(Fu)}function Id({value:e,children:t}){return ue.createElement(Fu.Provider,{value:e},t)}var ie=(e=>(e.Space=" ",e.Enter="Enter",e.Escape="Escape",e.Backspace="Backspace",e.Delete="Delete",e.ArrowLeft="ArrowLeft",e.ArrowUp="ArrowUp",e.ArrowRight="ArrowRight",e.ArrowDown="ArrowDown",e.Home="Home",e.End="End",e.PageUp="PageUp",e.PageDown="PageDown",e.Tab="Tab",e))(ie||{});function fh(e,t,n){let[r,l]=y.useState(n),i=e!==void 0,o=y.useRef(i),u=y.useRef(!1),s=y.useRef(!1);return i&&!o.current&&!u.current?(u.current=!0,o.current=i,console.error("A component is changing from uncontrolled to controlled. This may be caused by the value changing from undefined to a defined value, which should not happen.")):!i&&o.current&&!s.current&&(s.current=!0,o.current=i,console.error("A component is changing from controlled to uncontrolled. This may be caused by the value changing from a defined value to undefined, which should not happen.")),[i?e:r,A(a=>(i||l(a),t?.(a)))]}function ia(e){return[e.screenX,e.screenY]}function ph(){let e=y.useRef([-1,-1]);return{wasMoved(t){let n=ia(t);return e.current[0]===n[0]&&e.current[1]===n[1]?!1:(e.current=n,!0)},update(t){e.current=ia(t)}}}function Fd(){let e=y.useRef(!1);return be(()=>(e.current=!0,()=>{e.current=!1}),[]),e}var hh=(e=>(e[e.Open=0]="Open",e[e.Closed=1]="Closed",e))(hh||{}),mh=(e=>(e[e.Single=0]="Single",e[e.Multi=1]="Multi",e))(mh||{}),vh=(e=>(e[e.Pointer=0]="Pointer",e[e.Other=1]="Other",e))(vh||{}),gh=(e=>(e[e.OpenListbox=0]="OpenListbox",e[e.CloseListbox=1]="CloseListbox",e[e.GoToOption=2]="GoToOption",e[e.Search=3]="Search",e[e.ClearSearch=4]="ClearSearch",e[e.RegisterOption=5]="RegisterOption",e[e.UnregisterOption=6]="UnregisterOption",e[e.RegisterLabel=7]="RegisterLabel",e))(gh||{});function zi(e,t=n=>n){let n=e.activeOptionIndex!==null?e.options[e.activeOptionIndex]:null,r=th(t(e.options.slice()),i=>i.dataRef.current.domRef.current),l=n?r.indexOf(n):null;return l===-1&&(l=null),{options:r,activeOptionIndex:l}}let yh={[1](e){return e.dataRef.current.disabled||e.listboxState===1?e:{...e,activeOptionIndex:null,listboxState:1}},[0](e){if(e.dataRef.current.disabled||e.listboxState===0)return e;let t=e.activeOptionIndex,{isSelected:n}=e.dataRef.current,r=e.options.findIndex(l=>n(l.dataRef.current.value));return r!==-1&&(t=r),{...e,listboxState:0,activeOptionIndex:t}},[2](e,t){var n;if(e.dataRef.current.disabled||e.listboxState===1)return e;let r=zi(e),l=oh(t,{resolveItems:()=>r.options,resolveActiveIndex:()=>r.activeOptionIndex,resolveId:i=>i.id,resolveDisabled:i=>i.dataRef.current.disabled});return{...e,...r,searchQuery:"",activeOptionIndex:l,activationTrigger:(n=t.trigger)!=null?n:1}},[3]:(e,t)=>{if(e.dataRef.current.disabled||e.listboxState===1)return e;let n=e.searchQuery!==""?0:1,r=e.searchQuery+t.value.toLowerCase(),l=(e.activeOptionIndex!==null?e.options.slice(e.activeOptionIndex+n).concat(e.options.slice(0,e.activeOptionIndex+n)):e.options).find(o=>{var u;return!o.dataRef.current.disabled&&((u=o.dataRef.current.textValue)==null?void 0:u.startsWith(r))}),i=l?e.options.indexOf(l):-1;return i===-1||i===e.activeOptionIndex?{...e,searchQuery:r}:{...e,searchQuery:r,activeOptionIndex:i,activationTrigger:1}},[4](e){return e.dataRef.current.disabled||e.listboxState===1||e.searchQuery===""?e:{...e,searchQuery:""}},[5]:(e,t)=>{let n={id:t.id,dataRef:t.dataRef},r=zi(e,l=>[...l,n]);return e.activeOptionIndex===null&&e.dataRef.current.isSelected(t.dataRef.current.value)&&(r.activeOptionIndex=r.options.indexOf(n)),{...e,...r}},[6]:(e,t)=>{let n=zi(e,r=>{let l=r.findIndex(i=>i.id===t.id);return l!==-1&&r.splice(l,1),r});return{...e,...n,activationTrigger:1}},[7]:(e,t)=>({...e,labelId:t.id})},Mu=y.createContext(null);Mu.displayName="ListboxActionsContext";function _r(e){let t=y.useContext(Mu);if(t===null){let n=new Error(`<${e} /> is missing a parent component.`);throw Error.captureStackTrace&&Error.captureStackTrace(n,_r),n}return t}let ju=y.createContext(null);ju.displayName="ListboxDataContext";function Lr(e){let t=y.useContext(ju);if(t===null){let n=new Error(`<${e} /> is missing a parent component.`);throw Error.captureStackTrace&&Error.captureStackTrace(n,Lr),n}return t}function kh(e,t){return ce(t.type,yh,e,t)}let wh=y.Fragment,xh=vt(function(e,t){let{value:n,defaultValue:r,name:l,onChange:i,by:o=(D,j)=>D===j,disabled:u=!1,horizontal:s=!1,multiple:a=!1,...p}=e;const f=s?"horizontal":"vertical";let m=ln(t),[g=a?[]:void 0,v]=fh(n,i,r),[x,N]=y.useReducer(kh,{dataRef:y.createRef(),listboxState:1,options:[],searchQuery:"",labelId:null,activeOptionIndex:null,activationTrigger:1}),d=y.useRef({static:!1,hold:!1}),c=y.useRef(null),h=y.useRef(null),w=y.useRef(null),E=A(typeof o=="string"?(D,j)=>{let te=o;return D?.[te]===j?.[te]}:o),P=y.useCallback(D=>ce(C.mode,{[1]:()=>g.some(j=>E(j,D)),[0]:()=>E(g,D)}),[g]),C=y.useMemo(()=>({...x,value:g,disabled:u,mode:a?1:0,orientation:f,compare:E,isSelected:P,optionsPropsRef:d,labelRef:c,buttonRef:h,optionsRef:w}),[g,u,a,x]);be(()=>{x.dataRef.current=C},[C]),nh([C.buttonRef,C.optionsRef],(D,j)=>{var te;N({type:1}),Od(j,Iu.Loose)||(D.preventDefault(),(te=C.buttonRef.current)==null||te.focus())},C.listboxState===0);let T=y.useMemo(()=>({open:C.listboxState===0,disabled:u,value:g}),[C,u,g]),b=A(D=>{let j=C.options.find(te=>te.id===D);!j||Be(j.dataRef.current.value)}),$=A(()=>{if(C.activeOptionIndex!==null){let{dataRef:D,id:j}=C.options[C.activeOptionIndex];Be(D.current.value),N({type:2,focus:Ee.Specific,id:j})}}),le=A(()=>N({type:0})),Ve=A(()=>N({type:1})),He=A((D,j,te)=>D===Ee.Specific?N({type:2,focus:Ee.Specific,id:j,trigger:te}):N({type:2,focus:D,trigger:te})),on=A((D,j)=>(N({type:5,id:D,dataRef:j}),()=>N({type:6,id:D}))),rt=A(D=>(N({type:7,id:D}),()=>N({type:7,id:null}))),Be=A(D=>ce(C.mode,{[0](){return v?.(D)},[1](){let j=C.value.slice(),te=j.findIndex(Ie=>E(Ie,D));return te===-1?j.push(D):j.splice(te,1),v?.(j)}})),At=A(D=>N({type:3,value:D})),O=A(()=>N({type:4})),R=y.useMemo(()=>({onChange:Be,registerOption:on,registerLabel:rt,goToOption:He,closeListbox:Ve,openListbox:le,selectActiveOption:$,selectOption:b,search:At,clearSearch:O}),[]),z={ref:m},F=y.useRef(null),J=In();return y.useEffect(()=>{!F.current||r!==void 0&&J.addEventListener(F.current,"reset",()=>{Be(r)})},[F,Be]),ue.createElement(Mu.Provider,{value:R},ue.createElement(ju.Provider,{value:C},ue.createElement(Id,{value:ce(C.listboxState,{[0]:_e.Open,[1]:_e.Closed})},l!=null&&g!=null&&Td({[l]:g}).map(([D,j],te)=>ue.createElement(dh,{features:$d.Hidden,ref:te===0?Ie=>{var Ut;F.current=(Ut=Ie?.closest("form"))!=null?Ut:null}:void 0,...Fo({key:D,as:"input",type:"hidden",hidden:!0,readOnly:!0,name:D,value:j})})),jt({ourProps:z,theirProps:p,slot:T,defaultTag:wh,name:"Listbox"}))))}),Sh="button",Eh=vt(function(e,t){var n;let r=Xl(),{id:l=`headlessui-listbox-button-${r}`,...i}=e,o=Lr("Listbox.Button"),u=_r("Listbox.Button"),s=ln(o.buttonRef,t),a=In(),p=A(N=>{switch(N.key){case ie.Space:case ie.Enter:case ie.ArrowDown:N.preventDefault(),u.openListbox(),a.nextFrame(()=>{o.value||u.goToOption(Ee.First)});break;case ie.ArrowUp:N.preventDefault(),u.openListbox(),a.nextFrame(()=>{o.value||u.goToOption(Ee.Last)});break}}),f=A(N=>{switch(N.key){case ie.Space:N.preventDefault();break}}),m=A(N=>{if(sh(N.currentTarget))return N.preventDefault();o.listboxState===0?(u.closeListbox(),a.nextFrame(()=>{var d;return(d=o.buttonRef.current)==null?void 0:d.focus({preventScroll:!0})})):(N.preventDefault(),u.openListbox())}),g=Nd(()=>{if(o.labelId)return[o.labelId,l].join(" ")},[o.labelId,l]),v=y.useMemo(()=>({open:o.listboxState===0,disabled:o.disabled,value:o.value}),[o]),x={ref:s,id:l,type:rh(e,o.buttonRef),"aria-haspopup":"listbox","aria-controls":(n=o.optionsRef.current)==null?void 0:n.id,"aria-expanded":o.disabled?void 0:o.listboxState===0,"aria-labelledby":g,disabled:o.disabled,onKeyDown:p,onKeyUp:f,onClick:m};return jt({ourProps:x,theirProps:i,slot:v,defaultTag:Sh,name:"Listbox.Button"})}),Ch="label",Nh=vt(function(e,t){let n=Xl(),{id:r=`headlessui-listbox-label-${n}`,...l}=e,i=Lr("Listbox.Label"),o=_r("Listbox.Label"),u=ln(i.labelRef,t);be(()=>o.registerLabel(r),[r]);let s=A(()=>{var p;return(p=i.buttonRef.current)==null?void 0:p.focus({preventScroll:!0})}),a=y.useMemo(()=>({open:i.listboxState===0,disabled:i.disabled}),[i]);return jt({ourProps:{ref:u,id:r,onClick:s},theirProps:l,slot:a,defaultTag:Ch,name:"Listbox.Label"})}),Ph="ul",Oh=$l.RenderStrategy|$l.Static,_h=vt(function(e,t){var n;let r=Xl(),{id:l=`headlessui-listbox-options-${r}`,...i}=e,o=Lr("Listbox.Options"),u=_r("Listbox.Options"),s=ln(o.optionsRef,t),a=In(),p=In(),f=Du(),m=(()=>f!==null?(f&_e.Open)===_e.Open:o.listboxState===0)();y.useEffect(()=>{var d;let c=o.optionsRef.current;!c||o.listboxState===0&&c!==((d=Pd(c))==null?void 0:d.activeElement)&&c.focus({preventScroll:!0})},[o.listboxState,o.optionsRef]);let g=A(d=>{switch(p.dispose(),d.key){case ie.Space:if(o.searchQuery!=="")return d.preventDefault(),d.stopPropagation(),u.search(d.key);case ie.Enter:if(d.preventDefault(),d.stopPropagation(),o.activeOptionIndex!==null){let{dataRef:c}=o.options[o.activeOptionIndex];u.onChange(c.current.value)}o.mode===0&&(u.closeListbox(),tn().nextFrame(()=>{var c;return(c=o.buttonRef.current)==null?void 0:c.focus({preventScroll:!0})}));break;case ce(o.orientation,{vertical:ie.ArrowDown,horizontal:ie.ArrowRight}):return d.preventDefault(),d.stopPropagation(),u.goToOption(Ee.Next);case ce(o.orientation,{vertical:ie.ArrowUp,horizontal:ie.ArrowLeft}):return d.preventDefault(),d.stopPropagation(),u.goToOption(Ee.Previous);case ie.Home:case ie.PageUp:return d.preventDefault(),d.stopPropagation(),u.goToOption(Ee.First);case ie.End:case ie.PageDown:return d.preventDefault(),d.stopPropagation(),u.goToOption(Ee.Last);case ie.Escape:return d.preventDefault(),d.stopPropagation(),u.closeListbox(),a.nextFrame(()=>{var c;return(c=o.buttonRef.current)==null?void 0:c.focus({preventScroll:!0})});case ie.Tab:d.preventDefault(),d.stopPropagation();break;default:d.key.length===1&&(u.search(d.key),p.setTimeout(()=>u.clearSearch(),350));break}}),v=Nd(()=>{var d,c,h;return(h=(d=o.labelRef.current)==null?void 0:d.id)!=null?h:(c=o.buttonRef.current)==null?void 0:c.id},[o.labelRef.current,o.buttonRef.current]),x=y.useMemo(()=>({open:o.listboxState===0}),[o]),N={"aria-activedescendant":o.activeOptionIndex===null||(n=o.options[o.activeOptionIndex])==null?void 0:n.id,"aria-multiselectable":o.mode===1?!0:void 0,"aria-labelledby":v,"aria-orientation":o.orientation,id:l,onKeyDown:g,role:"listbox",tabIndex:0,ref:s};return jt({ourProps:N,theirProps:i,slot:x,defaultTag:Ph,features:Oh,visible:m,name:"Listbox.Options"})}),Lh="li",Th=vt(function(e,t){let n=Xl(),{id:r=`headlessui-listbox-option-${n}`,disabled:l=!1,value:i,...o}=e,u=Lr("Listbox.Option"),s=_r("Listbox.Option"),a=u.activeOptionIndex!==null?u.options[u.activeOptionIndex].id===r:!1,p=u.isSelected(i),f=y.useRef(null),m=ct({disabled:l,value:i,domRef:f,get textValue(){var E,P;return(P=(E=f.current)==null?void 0:E.textContent)==null?void 0:P.toLowerCase()}}),g=ln(t,f);be(()=>{if(u.listboxState!==0||!a||u.activationTrigger===0)return;let E=tn();return E.requestAnimationFrame(()=>{var P,C;(C=(P=f.current)==null?void 0:P.scrollIntoView)==null||C.call(P,{block:"nearest"})}),E.dispose},[f,a,u.listboxState,u.activationTrigger,u.activeOptionIndex]),be(()=>s.registerOption(r,m),[m,r]);let v=A(E=>{if(l)return E.preventDefault();s.onChange(i),u.mode===0&&(s.closeListbox(),tn().nextFrame(()=>{var P;return(P=u.buttonRef.current)==null?void 0:P.focus({preventScroll:!0})}))}),x=A(()=>{if(l)return s.goToOption(Ee.Nothing);s.goToOption(Ee.Specific,r)}),N=ph(),d=A(E=>N.update(E)),c=A(E=>{!N.wasMoved(E)||l||a||s.goToOption(Ee.Specific,r,0)}),h=A(E=>{!N.wasMoved(E)||l||!a||s.goToOption(Ee.Nothing)}),w=y.useMemo(()=>({active:a,selected:p,disabled:l}),[a,p,l]);return jt({ourProps:{id:r,ref:g,role:"option",tabIndex:l===!0?void 0:-1,"aria-disabled":l===!0?!0:void 0,"aria-selected":p,disabled:void 0,onClick:v,onFocus:x,onPointerEnter:d,onMouseEnter:d,onPointerMove:c,onMouseMove:c,onPointerLeave:h,onMouseLeave:h},theirProps:o,slot:w,defaultTag:Lh,name:"Listbox.Option"})}),Yr=Object.assign(xh,{Button:Eh,Label:Nh,Options:_h,Option:Th});function Rh(e=0){let[t,n]=y.useState(e),r=y.useCallback(u=>n(s=>s|u),[t]),l=y.useCallback(u=>Boolean(t&u),[t]),i=y.useCallback(u=>n(s=>s&~u),[n]),o=y.useCallback(u=>n(s=>s^u),[n]);return{flags:t,addFlag:r,hasFlag:l,removeFlag:i,toggleFlag:o}}function zh(e){let t={called:!1};return(...n)=>{if(!t.called)return t.called=!0,e(...n)}}function $i(e,...t){e&&t.length>0&&e.classList.add(...t)}function Ii(e,...t){e&&t.length>0&&e.classList.remove(...t)}function $h(e,t){let n=tn();if(!e)return n.dispose;let{transitionDuration:r,transitionDelay:l}=getComputedStyle(e),[i,o]=[r,l].map(u=>{let[s=0]=u.split(",").filter(Boolean).map(a=>a.includes("ms")?parseFloat(a):parseFloat(a)*1e3).sort((a,p)=>p-a);return s});if(i+o!==0){let u=n.addEventListener(e,"transitionend",s=>{s.target===s.currentTarget&&(t(),u())})}else t();return n.add(()=>t()),n.dispose}function Ih(e,t,n,r){let l=n?"enter":"leave",i=tn(),o=r!==void 0?zh(r):()=>{};l==="enter"&&(e.removeAttribute("hidden"),e.style.display="");let u=ce(l,{enter:()=>t.enter,leave:()=>t.leave}),s=ce(l,{enter:()=>t.enterTo,leave:()=>t.leaveTo}),a=ce(l,{enter:()=>t.enterFrom,leave:()=>t.leaveFrom});return Ii(e,...t.enter,...t.enterTo,...t.enterFrom,...t.leave,...t.leaveFrom,...t.leaveTo,...t.entered),$i(e,...u,...a),i.nextFrame(()=>{Ii(e,...a),$i(e,...s),$h(e,()=>(Ii(e,...u),$i(e,...t.entered),o()))}),i.dispose}function Fh({container:e,direction:t,classes:n,onStart:r,onStop:l}){let i=Fd(),o=In(),u=ct(t);be(()=>{let s=tn();o.add(s.dispose);let a=e.current;if(a&&u.current!=="idle"&&i.current)return s.dispose(),r.current(u.current),s.add(Ih(a,n.current,u.current==="enter",()=>{s.dispose(),l.current(u.current)})),s.dispose},[t])}function Vt(e=""){return e.split(" ").filter(t=>t.trim().length>1)}let Zl=y.createContext(null);Zl.displayName="TransitionContext";var Dh=(e=>(e.Visible="visible",e.Hidden="hidden",e))(Dh||{});function Mh(){let e=y.useContext(Zl);if(e===null)throw new Error("A is used but it is missing a parent or .");return e}function jh(){let e=y.useContext(Jl);if(e===null)throw new Error("A is used but it is missing a parent or .");return e}let Jl=y.createContext(null);Jl.displayName="NestingContext";function ql(e){return"children"in e?ql(e.children):e.current.filter(({el:t})=>t.current!==null).filter(({state:t})=>t==="visible").length>0}function Dd(e,t){let n=ct(e),r=y.useRef([]),l=Fd(),i=In(),o=A((g,v=st.Hidden)=>{let x=r.current.findIndex(({el:N})=>N===g);x!==-1&&(ce(v,{[st.Unmount](){r.current.splice(x,1)},[st.Hidden](){r.current[x].state="hidden"}}),i.microTask(()=>{var N;!ql(r)&&l.current&&((N=n.current)==null||N.call(n))}))}),u=A(g=>{let v=r.current.find(({el:x})=>x===g);return v?v.state!=="visible"&&(v.state="visible"):r.current.push({el:g,state:"visible"}),()=>o(g,st.Unmount)}),s=y.useRef([]),a=y.useRef(Promise.resolve()),p=y.useRef({enter:[],leave:[],idle:[]}),f=A((g,v,x)=>{s.current.splice(0),t&&(t.chains.current[v]=t.chains.current[v].filter(([N])=>N!==g)),t?.chains.current[v].push([g,new Promise(N=>{s.current.push(N)})]),t?.chains.current[v].push([g,new Promise(N=>{Promise.all(p.current[v].map(([d,c])=>c)).then(()=>N())})]),v==="enter"?a.current=a.current.then(()=>t?.wait.current).then(()=>x(v)):x(v)}),m=A((g,v,x)=>{Promise.all(p.current[v].splice(0).map(([N,d])=>d)).then(()=>{var N;(N=s.current.shift())==null||N()}).then(()=>x(v))});return y.useMemo(()=>({children:r,register:u,unregister:o,onStart:f,onStop:m,wait:a,chains:p}),[u,o,r,f,m,p,a])}function Ah(){}let Uh=["beforeEnter","afterEnter","beforeLeave","afterLeave"];function oa(e){var t;let n={};for(let r of Uh)n[r]=(t=e[r])!=null?t:Ah;return n}function bh(e){let t=y.useRef(oa(e));return y.useEffect(()=>{t.current=oa(e)},[e]),t}let Vh="div",Md=$l.RenderStrategy,jd=vt(function(e,t){let{beforeEnter:n,afterEnter:r,beforeLeave:l,afterLeave:i,enter:o,enterFrom:u,enterTo:s,entered:a,leave:p,leaveFrom:f,leaveTo:m,...g}=e,v=y.useRef(null),x=ln(v,t),N=g.unmount?st.Unmount:st.Hidden,{show:d,appear:c,initial:h}=Mh(),[w,E]=y.useState(d?"visible":"hidden"),P=jh(),{register:C,unregister:T}=P,b=y.useRef(null);y.useEffect(()=>C(v),[C,v]),y.useEffect(()=>{if(N===st.Hidden&&v.current){if(d&&w!=="visible"){E("visible");return}return ce(w,{hidden:()=>T(v),visible:()=>C(v)})}},[w,v,C,T,d,N]);let $=ct({enter:Vt(o),enterFrom:Vt(u),enterTo:Vt(s),entered:Vt(a),leave:Vt(p),leaveFrom:Vt(f),leaveTo:Vt(m)}),le=bh({beforeEnter:n,afterEnter:r,beforeLeave:l,afterLeave:i}),Ve=$u();y.useEffect(()=>{if(Ve&&w==="visible"&&v.current===null)throw new Error("Did you forget to passthrough the `ref` to the actual DOM node?")},[v,w,Ve]);let He=h&&!c,on=(()=>!Ve||He||b.current===d?"idle":d?"enter":"leave")(),rt=Rh(0),Be=A(F=>ce(F,{enter:()=>{rt.addFlag(_e.Opening),le.current.beforeEnter()},leave:()=>{rt.addFlag(_e.Closing),le.current.beforeLeave()},idle:()=>{}})),At=A(F=>ce(F,{enter:()=>{rt.removeFlag(_e.Opening),le.current.afterEnter()},leave:()=>{rt.removeFlag(_e.Closing),le.current.afterLeave()},idle:()=>{}})),O=Dd(()=>{E("hidden"),T(v)},P);Fh({container:v,classes:$,direction:on,onStart:ct(F=>{O.onStart(v,F,Be)}),onStop:ct(F=>{O.onStop(v,F,At),F==="leave"&&!ql(O)&&(E("hidden"),T(v))})}),y.useEffect(()=>{!He||(N===st.Hidden?b.current=null:b.current=d)},[d,He,w]);let R=g,z={ref:x};return c&&d&&zt.isServer&&(R={...R,className:_d(g.className,...$.current.enter,...$.current.enterFrom)}),ue.createElement(Jl.Provider,{value:O},ue.createElement(Id,{value:ce(w,{visible:_e.Open,hidden:_e.Closed})|rt.flags},jt({ourProps:z,theirProps:R,defaultTag:Vh,features:Md,visible:w==="visible",name:"Transition.Child"})))}),Do=vt(function(e,t){let{show:n,appear:r=!1,unmount:l,...i}=e,o=y.useRef(null),u=ln(o,t);$u();let s=Du();if(n===void 0&&s!==null&&(n=(s&_e.Open)===_e.Open),![!0,!1].includes(n))throw new Error("A is used but it is missing a `show={true | false}` prop.");let[a,p]=y.useState(n?"visible":"hidden"),f=Dd(()=>{p("hidden")}),[m,g]=y.useState(!0),v=y.useRef([n]);be(()=>{m!==!1&&v.current[v.current.length-1]!==n&&(v.current.push(n),g(!1))},[v,n]);let x=y.useMemo(()=>({show:n,appear:r,initial:m}),[n,r,m]);y.useEffect(()=>{if(n)p("visible");else if(!ql(f))p("hidden");else{let d=o.current;if(!d)return;let c=d.getBoundingClientRect();c.x===0&&c.y===0&&c.width===0&&c.height===0&&p("hidden")}},[n,f]);let N={unmount:l};return ue.createElement(Jl.Provider,{value:f},ue.createElement(Zl.Provider,{value:x},jt({ourProps:{...N,as:y.Fragment,children:ue.createElement(jd,{ref:u,...N,...i})},theirProps:{},defaultTag:y.Fragment,features:Md,visible:a==="visible",name:"Transition"})))}),Hh=vt(function(e,t){let n=y.useContext(Zl)!==null,r=Du()!==null;return ue.createElement(ue.Fragment,null,!n&&r?ue.createElement(Do,{ref:t,...e}):ue.createElement(jd,{ref:t,...e}))}),Bh=Object.assign(Do,{Child:Hh,Root:Do});function Wh({options:e,selectedOption:t,setSelected:n,label:r,size:l="large",testId:i,disabled:o}){let u="";switch(l){case"small":u="py-1 bg-white dark:bg-slate-900 hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-500 dark:text-slate-400 dark:ring-offset-slate-900";break;case"medium":u="py-2 bg-white text-slate-400 hover:bg-slate-100";break;case"large":u="py-3 bg-violet-800 text-white hover:bg-violet-900";break}return k(Yr,{value:t,onChange:n,children:({open:s})=>L("div",{"data-test":i,className:W("relative",o&&"opacity-60 cursor-not-allowed"),children:[r&&k(Cd,{className:"w-full inline-block pb-px",children:r}),L("div",{className:"relative",children:[k("div",{className:"rounded-lg w-full",children:L("div",{className:"relative z-0 inline-flex rounded-lg w-full border dark:border-slate-700",children:[k("div",{className:"relative flex flex-grow items-center bg-white dark:bg-slate-900 pl-3 pr-4 border border-transparent rounded-l-lg text-slate-700 dark:text-slate-300",children:k("p",{className:"ml-2.5 font-medium",children:(e.find(a=>a.value===t)??e[0])?.display??"make a selection..."})}),L(Yr.Button,{className:W("relative inline-flex items-center px-4 rounded-l-none rounded-r-lg text-sm font-medium focus:outline-none focus:z-10 focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-50 focus:ring-indigo-500 transition-[outline,background-color] duration-100",u,o&&"cursor-not-allowed pointer-events-none"),children:[k("span",{className:"sr-only",children:"Change published status"}),k("i",{className:`fa fa-chevron-down text-xl text-opacity-90 transition-transform duration-100 ${s?"-rotate-180":"rotate-0"}`,"aria-hidden":"true"})]})]})}),k(Bh,{show:s,as:y.Fragment,leave:"transition-[opacity,transform] ease-in duration-100",leaveFrom:"opacity-100",leaveTo:"opacity-0 -translate-y-1",enter:"transition-[opacity,transform] ease-in duration-100",enterFrom:"opacity-0 -translate-y-1",enterTo:"opacity-100",children:k(Yr.Options,{className:"origin-top-right absolute z-20 right-0 mt-2 p-2 w-72 rounded-xl shadow-lg overflow-hidden bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 focus:outline-none flex flex-col",children:e.map(({value:a,display:p})=>k(Yr.Option,{className:({active:f})=>W(f?"bg-violet-100 text-slate-900 dark:text-slate-200":"text-slate-900 dark:text-slate-300","cursor-pointer select-none relative rounded-lg transition-[background-color] duration-75",l==="small"?"py-2 px-3":"p-3.5 text-md",l==="small"&&f&&"bg-slate-50 dark:bg-slate-700/50"),value:a,children:({selected:f})=>k("div",{className:"flex flex-col",children:L("div",{className:"flex justify-between",children:[k("p",{className:f?"font-semibold":"font-normal",children:p}),f?k("span",{className:"text-violet-700",children:k("i",{className:"fa fa-check h-5 w-5","aria-hidden":"true"})}):null]})})},a))})})]})]})})}const Qh=({type:e,className:t,children:n,size:r})=>{let l="";switch(e){case"green":case"ok":l="bg-green-50 dark:bg-green-500/10 border-green-200 dark:border-green-500/50 text-green-600 dark:text-green-300";break;case"red":case"error":l="bg-red-50 dark:bg-red-500/10 border-red-200 dark:border-red-500/50 text-red-600 dark:text-red-300";break;case"yellow":case"warning":l="bg-yellow-50 dark:bg-yellow-500/10 border-yellow-200 dark:border-yellow-500/50 text-yellow-600 dark:text-yellow-300";break;case"blue":case"info":l="bg-blue-50 dark:bg-blue-500/10 border-blue-200 dark:border-blue-500/50 text-blue-600 dark:text-blue-300";break}return k("div",{className:W("max-w-fit border rounded-full flex justify-center items-center",{"text-xs px-[12px] py-[2px]":r==="small","text-base px-6 py-0.5":r==="large","text-sm px-[14px] py-[2.5px]":r==="medium"||!r},l,t),children:n})},Kh=({short:e,onRecheck:t,onDisconnect:n})=>{const[r,l]=y.useState(!1),[i,o]=y.useState(!1);return k("div",{className:W("min-h-screen flex flex-col justify-center items-center bg-white dark:bg-slate-900",e?"p-4":"p-8"),children:L("div",{className:W("border border-red-100 dark:border-red-500/40 rounded-2xl h-full flex flex-col bg-red-50/50 dark:bg-red-500/20",e?"p-6":"p-8"),children:[L("div",{className:"flex-grow flex flex-col space-y-4 text-slate-900 dark:text-slate-100",children:[L("h1",{className:"text-2xl font-medium",children:["Your Gertrude account is ",k("strong",{children:"no longer active."})]}),!e&&k("p",{children:"The internet filter will continue protecting this computer according to the rules set before the account went inactive, but no changes or suspensions can be made until the account is restored."}),L("p",{children:["To ",k("strong",{children:"restore the account,"})," login to the Gertrude parent site and resolve the payment issue, then click the ",k("strong",{children:"Recheck"})," ","button below."]}),!e&&L("p",{children:["If you no longer wish to use Gertrude, click the ",k("strong",{children:"Disconnect"})," ","button below, then uninstall the app."]}),L("p",{children:["Contact us at at"," ",k("a",{href:"https://gertrude.app/contact",className:"font-semibold text-slate-800 dark:text-slate-200 border-b-2 pb-1 border-transparent dark:border-transparent hover:border-slate-600 dark:hover:border-slate-100 hover:pb-0.5 duration-200 transition-[padding-bottom,border-color]",children:"https://gertrude.app/contact"})," ","to get help."]})]}),L("div",{className:"flex items-center mt-6 space-x-4 bg-white dark:bg-slate-900 p-4 rounded-xl self-stretch justify-between border border-red-100 dark:border-red-500/50",children:[L(q,{type:"button",onClick:()=>{l(!0),setTimeout(()=>l(!1),3e3),t()},color:"secondary",disabled:r,children:[k("i",{className:W("fa-solid fa-sync mr-3",r&&"animate-spin")}),r?"Rechecking...":"Recheck"]}),L(q,{type:"button",onClick:()=>{o(!0),setTimeout(()=>o(!1),6e3),n()},color:"warning",disabled:i,children:[k("i",{className:W("fa-solid mr-3",i?"fa-sync animate-spin":"fa-plug")}),i?"Disconnecting...":"Disconnect"]})]})]})})},Gh=({screen:e,setScreen:t})=>{const[n,r]=y.useState(0);return L("nav",{className:W("border-slate-200 dark:border-slate-800 border-r p-2 font-bold flex flex-col items-stretch space-y-1 bg-white dark:bg-slate-900 fixed h-full z-20 top-0"),children:[k(Xr,{isActive:e==="healthCheck",onClick:()=>t("healthCheck"),icon:"heart-pulse"}),k(Xr,{isActive:e==="actions",onClick:()=>t("actions"),icon:"arrow-pointer"}),k(Xr,{isActive:e==="exemptUsers",onClick:()=>t("exemptUsers"),icon:"users"}),k(Xr,{isActive:e==="advanced",onClick:()=>{n<10?r(n+1):t("advanced")},icon:"flask",className:n<10?"cursor-default opacity-0":""})]})},Xr=({onClick:e,isActive:t,className:n,icon:r})=>k("button",{onClick:e,className:W("transition-colors duration-100 w-12 h-12 flex justify-center items-center rounded-lg",t?"bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400":"text-slate-500 hover:bg-slate-50 dark:hover:bg-slate-800/50 hover:text-slate-600 dark:hover:text-slate-400",n),children:k("i",{className:`fa-solid fa-${r} text-2xl`})}),Yh=({filterRunning:e,installedAppVersion:t,availableAppUpdate:n,dangerZoneModal:r,userName:l,emit:i,quitting:o,releaseChannel:u,dispatch:s})=>{const a=Xh(n);return L("div",{className:"p-4 h-full flex flex-col justify-between relative",children:[k("div",{className:W("w-full h-full left-0 top-0 bg-slate-100 dark:bg-slate-900 z-10 transition-[backdrop-filter,background-color] duration-300 flex justify-center items-center fixed",r!=="hidden"?"bg-opacity-30 dark:bg-opacity-90 dark:backdrop-blur-sm backdrop-blur-md pointer-events-auto":"bg-opacity-0 dark:bg-opacity-0 backdrop-blur-none pointer-events-none"),onClick:()=>s({type:"dangerZoneModalDismissed"}),children:L("div",{className:W("bg-white dark:bg-slate-800 shadow-lg shadow-slate-300/50 dark:shadow-black/20 rounded-2xl transition-[transform,opacity] duration-300",r!=="hidden"?"pointer-events-auto":"pointer-events-none scale-75 opacity-0"),onClick:p=>p.stopPropagation(),children:[L("div",{className:"p-8",children:[L("h3",{className:"font-bold text-xl text-slate-900 dark:text-slate-200 ",children:["Are you sure you want to",r==="quitApp"?" quit Gertrude?":" stop the filter?"]}),k("p",{className:"max-w-md text-sm text-slate-500 dark:text-slate-400 mt-4",children:r==="quitApp"?"Quitting the app stops all screenshot and keystroke monitoring. This is usually only necessary when uninstalling or troubleshooting.":'Stopping the filter gives all users on this computer unrestricted internet access. If you want to temporarily suspend the filter, use the "Suspend filter" button in the main menubar dropdown instead.'})]}),L("div",{className:"p-4 bg-slate-50 dark:bg-slate-900/50 rounded-b-2xl flex justify-between space-x-4",children:[k(q,{type:"button",onClick:()=>s({type:"dangerZoneModalDismissed"}),color:"tertiary",className:"flex-grow",children:"Cancel"}),k(q,{type:"button",onClick:()=>i({case:r==="quitApp"?"confirmQuitAppClicked":"confirmStopFilterClicked"}),color:"warning",className:"flex-grow",disabled:r==="quitApp"&&o,children:o?"Quitting...":`I understand, ${r==="quitApp"?"quit the app":"stop the filter"}`})]})]})}),L("div",{className:"flex flex-col flex-grow",children:[L("div",{className:"border border-slate-200 dark:border-slate-800 rounded-2xl relative flex flex-col justify-between mb-3.5",children:[k(Qh,{type:a.badgeColor,className:"absolute right-2 top-2 w-36 !max-w-none",children:a.badgeText}),L("div",{className:"p-4 pt-3",children:[L("h2",{className:"text-lg font-semibold text-slate-600 dark:text-slate-300",children:["Currently running Gertrude version"," ",k("span",{className:"font-bold *font-mono text-violet-700 dark:text-violet-400",children:t})]}),k("p",{className:"text-slate-500 mt-2",children:a.versionMessage})]}),L("div",{className:"p-3 bg-slate-50 dark:bg-slate-800/50 rounded-b-2xl flex justify-end items-center",children:[L("p",{className:W("text-slate-500 opacity-80 italic ml-3 flex-grow",u==="stable"&&"hidden"),children:[k("i",{className:"fas fa-flask mr-2"}),"Release channel: ",k("b",{children:u})]}),L(q,{type:"button",size:"small",onClick:()=>i({case:"updateAppNowClicked"}),color:"tertiary",disabled:!n,children:[k("i",{className:"fas fa-sync-alt mr-2"}),"Update now"]})]})]}),l!==void 0&&L("div",{className:"border border-slate-200 dark:border-slate-800 rounded-2xl relative flex flex-col justify-between",children:[L("div",{className:"p-4 pt-3",children:[L("h2",{className:"text-lg font-semibold text-slate-600 dark:text-slate-300",children:["Connected to child:"," ",k("span",{className:"font-bold *font-mono text-violet-700 dark:text-violet-400",children:l})]}),k("p",{className:"text-slate-500 mt-2",children:"Disconnect if you want to connect a different child."})]}),k("div",{className:"p-3 bg-slate-50 dark:bg-slate-800/50 rounded-b-2xl flex justify-end",children:L(q,{type:"button",size:"small",onClick:()=>i({case:"disconnectUserClicked"}),color:"tertiary",children:[k("i",{className:"fa fa-scissors mr-2"}),"Disconnect"]})})]})]}),L("div",{className:"border border-red-200/70 dark:border-red-500/20 rounded-2xl bg-red-50/20 dark:bg-red-500/5",children:[L("div",{className:"p-4 pt-3",children:[k("h2",{className:"text-xl font-bold text-red-700 dark:text-red-400",children:"Danger zone"}),k("p",{className:"mt-2 text-red-700/70 dark:text-red-400/50",children:"These actions disable Gertrude‘s protections. Use only for troubleshooting or uninstalling."})]}),L("div",{className:"flex justify-end space-x-4 bg-red-50/30 dark:bg-red-500/5 rounded-b-2xl p-3",children:[e&&k(q,{type:"button",size:"small",onClick:()=>s({type:"dangerZoneStopFilterClicked"}),color:"warning",children:"Stop filter"}),k(q,{type:"button",size:"small",onClick:()=>s({type:"dangerZoneQuitAppClicked"}),color:"warning",children:"Quit app"})]})]})]})};function Xh(e){return e?e.required?{badgeColor:"red",badgeText:"Update required",versionMessage:L(ma,{children:["Update to required version ",k("b",{children:e.semver})," as soon as possible."]})}:{badgeColor:"yellow",badgeText:"Update available",versionMessage:`Gertrude version ${e.semver} is available for download.`}:{badgeColor:"green",badgeText:"Up to date",versionMessage:"We'll let you know when the next update is available."}}const Zh=e=>{let t,n;switch(e.state){case"ok":t="bg-green-500",n="fa-solid fa-check translate-x-[0.5px]";break;case"fail":t="bg-red-500",n="fa-solid fa-times translate-x-[0.5px]";break;case"warn":t="bg-yellow-400",n="fa-solid fa-minus";break;case"checking":t="bg-purple-500 dark:bg-purple-800",n="fa-solid fa-sync animate-spin dark:text-slate-100";break;case"unexpected":t="bg-gray-500/90",n="fa-solid fa-exclamation translate-x-[0.5px]";break}const r=Jh(e),l=qh(e);return L("div",{className:"flex items-center p-2 rounded-xl bg-slate-50 dark:bg-slate-800/30",children:[k("div",{className:W("w-6 h-6 rounded-full flex justify-center items-center",t),children:k("i",{className:W("text-white dark:text-slate-900",n)})}),L("div",{className:"flex-grow ml-4",children:[k("h3",{className:"font-medium text-slate-800 dark:text-slate-200",children:e.title}),l&&k("p",{className:"text-slate-500 dark:text-slate-400",dangerouslySetInnerHTML:{__html:l}})]}),r&&L(q,{type:"button",onClick:()=>e.emit(r.action),color:"tertiary",size:"small",children:[k("i",{className:`fa-solid fa-${r.icon} mr-2`}),r.label]})]})};function Jh(e){if(e.state==="warn"||e.state==="fail")return e.button}function qh(e){switch(e.state){case"ok":return e.message;case"fail":return e.message;case"warn":return e.message;case"unexpected":return e.message??"Unexpected check error, please try again";default:return}}function em(e){return Object.entries(e)}function tm(e){return e!=null}class nm{constructor(t,n,r,l){this.data=t,this.installedAppVersion=n,this.screenshotMonitoringEnabled=r,this.keystrokeMonitoringEnabled=l}get items(){return[this.appVersion,...this.filterItems,this.screenRecordingPermission,this.keystrokeRecordingPermission,this.notificationsPermission,this.macOsUserType,this.accountStatus].filter(tm)}get failingChecksCount(){return this.items.filter(({state:t})=>t==="fail").length}get isChecking(){return this.items.some(({state:t})=>t==="checking")}get appVersion(){const{latestAppVersion:t}=this.data;return t===void 0?{title:"App Version",state:"checking"}:Kr(t)?{title:"App Version",state:"unexpected",message:g0(t)}:this.installedAppVersion===t.value||this.installedAppVersion>t.value?{title:"App Version",state:"ok",message:`You're up to date (${this.installedAppVersion})`}:{title:"App Version",state:"warn",message:`Update available (${t.value})`,button:{icon:"sync",label:"Update",action:"upgradeAppClicked"}}}get screenRecordingPermission(){if(this.screenshotMonitoringEnabled)return this.data.screenRecordingPermissionOk===void 0?{title:"Screen recording permission",state:"checking"}:this.data.screenRecordingPermissionOk?{title:"Screen recording permission",state:"ok"}:{title:"Screen recording permission",state:"fail",message:"Gertrude can't take screenshots until you give permission",button:{icon:"cog",label:"Fix permission",action:"fixScreenRecordingPermissionClicked"}}}get keystrokeRecordingPermission(){if(this.keystrokeMonitoringEnabled)return this.data.keystrokeRecordingPermissionOk===void 0?{title:"Keystroke recording permission",state:"checking"}:this.data.keystrokeRecordingPermissionOk?{title:"Keystroke recording permission",state:"ok"}:{title:"Keystroke recording permission",state:"fail",message:"Gertrude can't monitor keystrokes until you give permission",button:{icon:"cog",label:"Fix permission",action:"fixKeystrokeRecordingPermissionClicked"}}}get macOsUserType(){return this.data.macOsUserType===void 0?{title:"macOS user account type",state:"checking"}:Kr(this.data.macOsUserType)?{title:"macOS user account type",state:"unexpected"}:this.data.macOsUserType.value!=="standard"?{title:"Mac user has admin privileges",state:"fail",message:"Admin users can disable Gertrude if they have the password",button:{icon:"user",label:"Remove admin privilege",action:"removeUserAdminPrivilegeClicked"}}:{title:"macOS user account type",state:"ok"}}get notificationsPermission(){switch(this.data.notificationsSetting){case"alert":return{title:"Notification settings",state:"ok"};case"banner":return{title:"Notification settings",state:"warn",message:'Set to "banner", recommended setting is "alert"',button:{icon:"cog",label:"Fix setting",action:"fixNotificationPermissionClicked"}};case"none":return{title:"Notification settings",state:"fail",message:"Notifications disabled, child will miss critical updates",button:{icon:"cog",label:"Fix setting",action:"fixNotificationPermissionClicked"}};default:return{title:"Notification settings",state:"checking"}}}get accountStatus(){if(this.data.accountStatus===void 0)return{title:"Gertrude account status",state:"checking"};if(Kr(this.data.accountStatus))return{title:"Gertrude account status",state:"unexpected"};switch(this.data.accountStatus.value){case"active":return{title:"Gertrude account status",state:"ok"};case"needsAttention":return{title:"Gertrude account status",state:"warn",message:"Needs attention: log in to the Gertrude parents website for more details"};default:return{title:"Gertrude account status",state:"fail",message:"Log in to the Gertrude parents website to resolve"}}}get filterItems(){const{filterStatus:t,latestAppVersion:n}=this.data;if(t===void 0||n===void 0||t.case==="installing"||t.case==="communicationBroken"&&t.repairing)return[{title:"Filter status",state:"checking"}];if(t.case==="disabled")return[{title:"Filter status",state:"warn",message:"Filter has been disabled",button:{icon:"cog",label:"Enable filter",action:"enableFilterClicked"}}];if(t.case==="notInstalled")return[{title:"Filter status",state:"warn",message:"Filter has not been installed",button:{icon:"cog",label:"Install filter",action:"installFilterClicked"}}];if(t.case==="installTimeout")return[{title:"Filter status",state:"unexpected",message:"Installation did not complete, try again"}];if(t.case==="unexpected")return[{title:"Filter status",state:"unexpected",message:"Unexpected error: try rebooting the computer"}];if(t.case==="communicationBroken")return[{title:"Filter to app communication broken",state:"fail",message:"If repair and recheck fails, restart the computer to resolve",button:{icon:"sync",label:"Attempt repair",action:"repairFilterCommunicationClicked"}}];const r=[],{version:l,numUserKeys:i}=t;return this.shouldShowFilterOutOfDateItem(n,l)&&r.push({title:"Filter version",state:"fail",message:`Filter version out of date (${l})`,button:{icon:"sync",label:"Reinstall filter",action:"repairOutOfDateFilterClicked"}}),i>0?r.push({title:"Filter rules",state:"ok",message:`Looks good, ${i} keys loaded`}):r.push({title:"Filter rules",state:"warn",message:"No keys loaded, try refreshing rules",button:{icon:"sync",label:"Refresh rules",action:"zeroKeysRefreshRulesClicked"}}),r}shouldShowFilterOutOfDateItem(t,n){return Kr(t)?n!==this.installedAppVersion:this.installedAppVersion!==t.value?!1:n!==this.installedAppVersion}}const rm=({installedAppVersion:e,screenshotMonitoringEnabled:t,keystrokeMonitoringEnabled:n,emit:r,...l})=>{const i=new nm(l,e,t,n);return L("div",{className:"h-full overflow-y-auto relative",children:[L("header",{className:"flex items-center justify-between border-b p-4 border-slate-200 dark:border-slate-800 sticky bg-white dark:bg-slate-900 top-0 z-10",children:[L("div",{children:[k("h2",{className:"text-2xl font-bold text-slate-800 dark:text-slate-100",children:"Health check"}),k("span",{className:W("text-slate-600 dark:text-slate-400",i.isChecking&&"italic opacity-50"),children:i.isChecking?"Checking...":i.failingChecksCount?`${i.failingChecksCount} ${B0("failing check",i.failingChecksCount)}!`:"Everything looks good!"})]}),L(q,{type:"button",onClick:()=>r("recheckClicked"),disabled:i.isChecking,color:"secondary",size:"small",children:[k("i",{className:W("fa-solid fa-sync mr-2",i.isChecking&&"animate-spin")}),i.isChecking?"Checking...":"Recheck"]})]}),k("ul",{className:"flex flex-col space-y-2 p-4",children:i.items.map(o=>k(Zh,{...o,emit:r},o.title))})]})},lm=({title:e,children:t,button:n,className:r})=>L("div",{className:W("flex flex-col items-center justify-center p-4 rounded-2xl border border-red-200 dark:border-red-700/50 bg-red-50/30 dark:bg-red-600/5 flex-grow",r),children:[k("span",{className:"font-bold text-lg text-slate-700 dark:text-white/80 mb-2",children:e}),t&&k("span",{className:"text-red-500 mb-4 dark:text-red-400",children:t}),n&&L(q,{type:"button",onClick:n.action,color:"warning",size:"small",children:[k("i",{className:`fa-solid ${n.icon} mr-2`}),n.text]})]}),im=({users:e,emit:t})=>e?e.case==="error"?k("div",{className:"p-6",children:k(lm,{title:"Unexpected error",button:{text:"Check health",icon:"fa-heart-pulse",action:()=>t({case:"gotoScreenClicked",screen:"healthCheck"})},children:"Check health, or contact support if the problem persists."})}):L("div",{className:"flex flex-col h-full",children:[k("header",{className:"flex items-center justify-between border-b p-4 border-slate-200 sticky bg-white dark:border-slate-800 dark:bg-slate-900 top-0",children:k("h2",{className:"text-2xl font-bold text-slate-800 dark:text-slate-100",children:"Exempt users"})}),L("main",{className:"p-4 flex-grow flex flex-col relative",children:[L("div",{className:"mr-4",children:[L("p",{className:"text-slate-500 dark:text-slate-400",children:["Gertrude's network filter has to make decisions about whether to allow or deny network requests from every user on this computer. For maximum internet safety, it defaults to blocking all requests for users that it doesn't have rules for. If this computer has another user or users who should have unrestricted internet access (like a parent's admin account on a shared computer), you can make that user"," ",k("strong",{className:"text-slate-700 dark:text-slate-200",children:"exempt from filtering"})," ","by selecting the user name below."]}),L("p",{className:"text-slate-500 dark:text-slate-400 mt-4",children:[k("strong",{className:"text-slate-700 dark:text-slate-200",children:"Please note:"})," ","any user that is exempt from filtering should have a password enabled that is unknown to any individual subject to filtering, or else they would be able to log in to that user at any time and also have unrestricted internet access."]})]}),k("ul",{className:"mt-4 space-y-2 flex-grow",children:e.value.map(n=>k(om,{name:n.name,isExempt:n.isExempt,onToggle:()=>t({case:"setUserExemption",userId:n.id,enabled:!n.isExempt})},n.id))}),k("div",{className:"flex justify-end mt-4",children:L(q,{type:"button",onClick:()=>t({case:"administrateOSUserAccountsClicked"}),color:"secondary",size:"medium",className:"",children:["Administrate user accounts",k("i",{className:"fa-solid fa-arrow-right ml-2"})]})})]})]}):null,om=({name:e,isExempt:t,onToggle:n})=>L("div",{onClick:n,className:W("flex items-center justify-start rounded-xl p-2 pl-4",t&&"bg-red-50 dark:bg-red-500/10"),children:[k("button",{className:W("w-5 h-5 rounded-full border-slate-300 dark:border-slate-700 border mr-4 flex justify-center items-center hover:scale-105 transition-[transform,border-color,border,background-color] duration-100",t&&"bg-red-500 !border-red-500 dark:border-red-500"),children:k("i",{className:"fa-solid fa-check text-white dark:text-slate-900 text-xs"})}),L("div",{className:"flex items-center space-x-2 grow",children:[k("h3",{className:"font-bold dark:text-white grow",children:e}),k("span",{className:"text-red-500 dark:text-red-400 pr-2",children:t?"exempt from filtering - unrestricted internet access":""})]})]}),um=({emit:e,pairqlEndpointDefault:t,pairqlEndpointOverride:n,websocketEndpointDefault:r,websocketEndpointOverride:l,appcastEndpointDefault:i,appcastEndpointOverride:o,appVersions:u})=>{const[s,a]=y.useState(n??""),[p,f]=y.useState(l??""),[m,g]=y.useState(o??""),[v,x]=y.useState(""),N=u?em(u).map(([d,c])=>({display:c,value:d})).sort((d,c)=>c.value.localeCompare(d.value)):null;return L("div",{className:"flex flex-col items-stretch h-full p-6 space-y-5",children:[L("div",{className:"pb-8 flex justify-between items-start",children:[N?L("div",{className:"flex space-x-2 items-end",children:[k(q,{size:"small",disabled:v==="",onClick:()=>e({case:"forceUpdateToSpecificVersionClicked",version:v}),type:"button",color:"tertiary",children:"Force update to:"}),k(Wh,{size:"small",options:[{display:"choose version...",value:""},...N],selectedOption:v,setSelected:x})]}):k("div",{className:"text-gray-400 italic",children:"Loading app versions..."}),k(q,{className:"h-12",onClick:()=>e({case:"deleteAllDeviceStorageClicked"}),type:"button",color:"warning",children:"Purge all device storage"})]}),L("div",{className:"flex items-end gap-x-2",children:[k(_i,{label:"API PairQL endpoint override:",type:"url",value:s,placeholder:t,setValue:a}),k(q,{className:"h-12",disabled:s.trim()==="",onClick:()=>e({case:"pairqlEndpointSet",url:s.trim()}),type:"button",color:"secondary",children:"Set"}),k(q,{className:"h-12",disabled:n===void 0,onClick:()=>e({case:"pairqlEndpointSet",url:void 0}),type:"button",color:"secondary",children:"Clear"})]}),L("div",{className:"flex items-end gap-x-2",children:[k(_i,{label:"Websocket endpoint override:",type:"url",value:p,placeholder:r,setValue:f}),k(q,{className:"h-12",disabled:p.trim()==="",onClick:()=>e({case:"websocketEndpointSet",url:p.trim()}),type:"button",color:"secondary",children:"Set"}),k(q,{className:"h-12",disabled:l===void 0,onClick:()=>e({case:"websocketEndpointSet",url:void 0}),type:"button",color:"secondary",children:"Clear"})]}),L("div",{className:"flex items-end gap-x-2",children:[k(_i,{label:"Sparkle Appcast endpoint override:",type:"url",value:m,placeholder:i,setValue:g}),k(q,{className:"h-12",disabled:m.trim()==="",onClick:()=>e({case:"appcastEndpointSet",url:m.trim()}),type:"button",color:"secondary",children:"Set"}),k(q,{className:"h-12",disabled:o===void 0,onClick:()=>e({case:"appcastEndpointSet",url:void 0}),type:"button",color:"secondary",children:"Clear"})]})]})};class sm extends y0{appState(){return{windowOpen:!0,screen:"healthCheck",filterState:{case:"off"},installedAppVersion:"0.0.0",healthCheck:{},releaseChannel:"stable",quitting:!1}}viewState(){return{filterSuspensionDurationInSeconds:String(60*5),dangerZoneModal:"hidden"}}initializer(){return{...this.appState(),...this.viewState()}}reducer(t,n){switch(n.type){case"filterSuspensionDurationInSecondsChanged":return{...t,filterSuspensionDurationInSeconds:n.value};case"receivedUpdatedAppState":return{...t,...n.appState};case"dangerZoneStopFilterClicked":return{...t,dangerZoneModal:"stopFilter"};case"dangerZoneQuitAppClicked":return{...t,dangerZoneModal:"quitApp"};case"dangerZoneModalDismissed":return{...t,dangerZoneModal:"hidden"};case"appEventEmitted":return n.event.case==="confirmStopFilterClicked"?{...t,dangerZoneModal:"hidden"}:t}}}const am=new sm,cm=({healthCheck:e,filterState:t,user:n,installedAppVersion:r,availableAppUpdate:l,releaseChannel:i,exemptableUsers:o,screen:u,advanced:s,quitting:a,dangerZoneModal:p,emit:f,dispatch:m})=>{let g;switch(u){case"healthCheck":g=k(rm,{...e,installedAppVersion:r,screenshotMonitoringEnabled:n?.keystrokeMonitoringEnabled??!1,keystrokeMonitoringEnabled:n?.screenshotMonitoringEnabled??!1,emit:v=>f({case:"healthCheck",action:v})});break;case"actions":g=k(Yh,{releaseChannel:i,emit:f,dispatch:m,filterRunning:t.case==="on",installedAppVersion:r,availableAppUpdate:l,dangerZoneModal:p,userName:n?.name,quitting:a});break;case"exemptUsers":g=k(im,{emit:f,users:o});break;case"advanced":g=s?k(um,{...s,emit:v=>f({case:"advanced",action:v})}):k(ma,{children:"Loading..."});break}return v0(e.accountStatus)==="inactive"?k(Kh,{onRecheck:()=>f({case:"inactiveAccountRecheckClicked"}),onDisconnect:()=>f({case:"inactiveAccountDisconnectAppClicked"})}):k("div",{className:"flex flex-col h-screen",children:L("div",{className:W("flex flex-grow relative"),children:[k(Gh,{screen:u,setScreen:v=>f({case:"gotoScreenClicked",screen:v})}),k("main",{className:"flex-grow bg-white dark:bg-slate-900 ml-16",children:g})]})})},dm=k0(am,cm);Di.createRoot(document.getElementById("app")).render(k(dm,{})); +`));let m=_d((l=p.props)==null?void 0:l.className,s.className),g=m?{className:m}:{};return y.cloneElement(p,Object.assign({},Ld(p.props,Fo(Ri(s,["ref"]))),f,a,uh(p.ref,a.ref),g))}return y.createElement(i,Object.assign({},Ri(s,["ref"]),i!==y.Fragment&&a,i!==y.Fragment&&f),p)}function uh(...e){return{ref:e.every(t=>t==null)?void 0:t=>{for(let n of e)n!=null&&(typeof n=="function"?n(t):n.current=t)}}}function Ld(...e){if(e.length===0)return{};if(e.length===1)return e[0];let t={},n={};for(let r of e)for(let l in r)l.startsWith("on")&&typeof r[l]=="function"?(n[l]!=null||(n[l]=[]),n[l].push(r[l])):t[l]=r[l];if(t.disabled||t["aria-disabled"])return Object.assign(t,Object.fromEntries(Object.keys(n).map(r=>[r,void 0])));for(let r in n)Object.assign(t,{[r](l,...i){let o=n[r];for(let u of o){if((l instanceof Event||l?.nativeEvent instanceof Event)&&l.defaultPrevented)return;u(l,...i)}}});return t}function vt(e){var t;return Object.assign(y.forwardRef(e),{displayName:(t=e.displayName)!=null?t:e.name})}function Fo(e){let t=Object.assign({},e);for(let n in t)t[n]===void 0&&delete t[n];return t}function Ri(e,t=[]){let n=Object.assign({},e);for(let r of t)r in n&&delete n[r];return n}function sh(e){let t=e.parentElement,n=null;for(;t&&!(t instanceof HTMLFieldSetElement);)t instanceof HTMLLegendElement&&(n=t),t=t.parentElement;let r=t?.getAttribute("disabled")==="";return r&&ah(n)?!1:r}function ah(e){if(!e)return!1;let t=e.previousElementSibling;for(;t!==null;){if(t instanceof HTMLLegendElement)return!1;t=t.previousElementSibling}return!0}function Td(e={},t=null,n=[]){for(let[r,l]of Object.entries(e))zd(n,Rd(t,r),l);return n}function Rd(e,t){return e?e+"["+t+"]":t}function zd(e,t,n){if(Array.isArray(n))for(let[r,l]of n.entries())zd(e,Rd(t,r.toString()),l);else n instanceof Date?e.push([t,n.toISOString()]):typeof n=="boolean"?e.push([t,n?"1":"0"]):typeof n=="string"?e.push([t,n]):typeof n=="number"?e.push([t,`${n}`]):n==null?e.push([t,""]):Td(n,t,e)}let ch="div";var $d=(e=>(e[e.None=1]="None",e[e.Focusable=2]="Focusable",e[e.Hidden=4]="Hidden",e))($d||{});let dh=vt(function(e,t){let{features:n=1,...r}=e,l={ref:t,"aria-hidden":(n&2)===2?!0:void 0,style:{position:"fixed",top:1,left:1,width:1,height:0,padding:0,margin:-1,overflow:"hidden",clip:"rect(0, 0, 0, 0)",whiteSpace:"nowrap",borderWidth:"0",...(n&4)===4&&(n&2)!==2&&{display:"none"}}};return jt({ourProps:l,theirProps:r,slot:{},defaultTag:ch,name:"Hidden"})}),Fu=y.createContext(null);Fu.displayName="OpenClosedContext";var _e=(e=>(e[e.Open=1]="Open",e[e.Closed=2]="Closed",e[e.Closing=4]="Closing",e[e.Opening=8]="Opening",e))(_e||{});function Du(){return y.useContext(Fu)}function Id({value:e,children:t}){return ue.createElement(Fu.Provider,{value:e},t)}var ie=(e=>(e.Space=" ",e.Enter="Enter",e.Escape="Escape",e.Backspace="Backspace",e.Delete="Delete",e.ArrowLeft="ArrowLeft",e.ArrowUp="ArrowUp",e.ArrowRight="ArrowRight",e.ArrowDown="ArrowDown",e.Home="Home",e.End="End",e.PageUp="PageUp",e.PageDown="PageDown",e.Tab="Tab",e))(ie||{});function fh(e,t,n){let[r,l]=y.useState(n),i=e!==void 0,o=y.useRef(i),u=y.useRef(!1),s=y.useRef(!1);return i&&!o.current&&!u.current?(u.current=!0,o.current=i,console.error("A component is changing from uncontrolled to controlled. This may be caused by the value changing from undefined to a defined value, which should not happen.")):!i&&o.current&&!s.current&&(s.current=!0,o.current=i,console.error("A component is changing from controlled to uncontrolled. This may be caused by the value changing from a defined value to undefined, which should not happen.")),[i?e:r,A(a=>(i||l(a),t?.(a)))]}function ia(e){return[e.screenX,e.screenY]}function ph(){let e=y.useRef([-1,-1]);return{wasMoved(t){let n=ia(t);return e.current[0]===n[0]&&e.current[1]===n[1]?!1:(e.current=n,!0)},update(t){e.current=ia(t)}}}function Fd(){let e=y.useRef(!1);return be(()=>(e.current=!0,()=>{e.current=!1}),[]),e}var hh=(e=>(e[e.Open=0]="Open",e[e.Closed=1]="Closed",e))(hh||{}),mh=(e=>(e[e.Single=0]="Single",e[e.Multi=1]="Multi",e))(mh||{}),vh=(e=>(e[e.Pointer=0]="Pointer",e[e.Other=1]="Other",e))(vh||{}),gh=(e=>(e[e.OpenListbox=0]="OpenListbox",e[e.CloseListbox=1]="CloseListbox",e[e.GoToOption=2]="GoToOption",e[e.Search=3]="Search",e[e.ClearSearch=4]="ClearSearch",e[e.RegisterOption=5]="RegisterOption",e[e.UnregisterOption=6]="UnregisterOption",e[e.RegisterLabel=7]="RegisterLabel",e))(gh||{});function zi(e,t=n=>n){let n=e.activeOptionIndex!==null?e.options[e.activeOptionIndex]:null,r=th(t(e.options.slice()),i=>i.dataRef.current.domRef.current),l=n?r.indexOf(n):null;return l===-1&&(l=null),{options:r,activeOptionIndex:l}}let yh={[1](e){return e.dataRef.current.disabled||e.listboxState===1?e:{...e,activeOptionIndex:null,listboxState:1}},[0](e){if(e.dataRef.current.disabled||e.listboxState===0)return e;let t=e.activeOptionIndex,{isSelected:n}=e.dataRef.current,r=e.options.findIndex(l=>n(l.dataRef.current.value));return r!==-1&&(t=r),{...e,listboxState:0,activeOptionIndex:t}},[2](e,t){var n;if(e.dataRef.current.disabled||e.listboxState===1)return e;let r=zi(e),l=oh(t,{resolveItems:()=>r.options,resolveActiveIndex:()=>r.activeOptionIndex,resolveId:i=>i.id,resolveDisabled:i=>i.dataRef.current.disabled});return{...e,...r,searchQuery:"",activeOptionIndex:l,activationTrigger:(n=t.trigger)!=null?n:1}},[3]:(e,t)=>{if(e.dataRef.current.disabled||e.listboxState===1)return e;let n=e.searchQuery!==""?0:1,r=e.searchQuery+t.value.toLowerCase(),l=(e.activeOptionIndex!==null?e.options.slice(e.activeOptionIndex+n).concat(e.options.slice(0,e.activeOptionIndex+n)):e.options).find(o=>{var u;return!o.dataRef.current.disabled&&((u=o.dataRef.current.textValue)==null?void 0:u.startsWith(r))}),i=l?e.options.indexOf(l):-1;return i===-1||i===e.activeOptionIndex?{...e,searchQuery:r}:{...e,searchQuery:r,activeOptionIndex:i,activationTrigger:1}},[4](e){return e.dataRef.current.disabled||e.listboxState===1||e.searchQuery===""?e:{...e,searchQuery:""}},[5]:(e,t)=>{let n={id:t.id,dataRef:t.dataRef},r=zi(e,l=>[...l,n]);return e.activeOptionIndex===null&&e.dataRef.current.isSelected(t.dataRef.current.value)&&(r.activeOptionIndex=r.options.indexOf(n)),{...e,...r}},[6]:(e,t)=>{let n=zi(e,r=>{let l=r.findIndex(i=>i.id===t.id);return l!==-1&&r.splice(l,1),r});return{...e,...n,activationTrigger:1}},[7]:(e,t)=>({...e,labelId:t.id})},Mu=y.createContext(null);Mu.displayName="ListboxActionsContext";function _r(e){let t=y.useContext(Mu);if(t===null){let n=new Error(`<${e} /> is missing a parent component.`);throw Error.captureStackTrace&&Error.captureStackTrace(n,_r),n}return t}let ju=y.createContext(null);ju.displayName="ListboxDataContext";function Lr(e){let t=y.useContext(ju);if(t===null){let n=new Error(`<${e} /> is missing a parent component.`);throw Error.captureStackTrace&&Error.captureStackTrace(n,Lr),n}return t}function kh(e,t){return ce(t.type,yh,e,t)}let wh=y.Fragment,xh=vt(function(e,t){let{value:n,defaultValue:r,name:l,onChange:i,by:o=(D,j)=>D===j,disabled:u=!1,horizontal:s=!1,multiple:a=!1,...p}=e;const f=s?"horizontal":"vertical";let m=ln(t),[g=a?[]:void 0,v]=fh(n,i,r),[x,N]=y.useReducer(kh,{dataRef:y.createRef(),listboxState:1,options:[],searchQuery:"",labelId:null,activeOptionIndex:null,activationTrigger:1}),d=y.useRef({static:!1,hold:!1}),c=y.useRef(null),h=y.useRef(null),w=y.useRef(null),E=A(typeof o=="string"?(D,j)=>{let te=o;return D?.[te]===j?.[te]}:o),P=y.useCallback(D=>ce(C.mode,{[1]:()=>g.some(j=>E(j,D)),[0]:()=>E(g,D)}),[g]),C=y.useMemo(()=>({...x,value:g,disabled:u,mode:a?1:0,orientation:f,compare:E,isSelected:P,optionsPropsRef:d,labelRef:c,buttonRef:h,optionsRef:w}),[g,u,a,x]);be(()=>{x.dataRef.current=C},[C]),nh([C.buttonRef,C.optionsRef],(D,j)=>{var te;N({type:1}),Od(j,Iu.Loose)||(D.preventDefault(),(te=C.buttonRef.current)==null||te.focus())},C.listboxState===0);let T=y.useMemo(()=>({open:C.listboxState===0,disabled:u,value:g}),[C,u,g]),b=A(D=>{let j=C.options.find(te=>te.id===D);!j||Be(j.dataRef.current.value)}),$=A(()=>{if(C.activeOptionIndex!==null){let{dataRef:D,id:j}=C.options[C.activeOptionIndex];Be(D.current.value),N({type:2,focus:Ee.Specific,id:j})}}),le=A(()=>N({type:0})),Ve=A(()=>N({type:1})),He=A((D,j,te)=>D===Ee.Specific?N({type:2,focus:Ee.Specific,id:j,trigger:te}):N({type:2,focus:D,trigger:te})),on=A((D,j)=>(N({type:5,id:D,dataRef:j}),()=>N({type:6,id:D}))),rt=A(D=>(N({type:7,id:D}),()=>N({type:7,id:null}))),Be=A(D=>ce(C.mode,{[0](){return v?.(D)},[1](){let j=C.value.slice(),te=j.findIndex(Ie=>E(Ie,D));return te===-1?j.push(D):j.splice(te,1),v?.(j)}})),At=A(D=>N({type:3,value:D})),O=A(()=>N({type:4})),R=y.useMemo(()=>({onChange:Be,registerOption:on,registerLabel:rt,goToOption:He,closeListbox:Ve,openListbox:le,selectActiveOption:$,selectOption:b,search:At,clearSearch:O}),[]),z={ref:m},F=y.useRef(null),J=In();return y.useEffect(()=>{!F.current||r!==void 0&&J.addEventListener(F.current,"reset",()=>{Be(r)})},[F,Be]),ue.createElement(Mu.Provider,{value:R},ue.createElement(ju.Provider,{value:C},ue.createElement(Id,{value:ce(C.listboxState,{[0]:_e.Open,[1]:_e.Closed})},l!=null&&g!=null&&Td({[l]:g}).map(([D,j],te)=>ue.createElement(dh,{features:$d.Hidden,ref:te===0?Ie=>{var Ut;F.current=(Ut=Ie?.closest("form"))!=null?Ut:null}:void 0,...Fo({key:D,as:"input",type:"hidden",hidden:!0,readOnly:!0,name:D,value:j})})),jt({ourProps:z,theirProps:p,slot:T,defaultTag:wh,name:"Listbox"}))))}),Sh="button",Eh=vt(function(e,t){var n;let r=Xl(),{id:l=`headlessui-listbox-button-${r}`,...i}=e,o=Lr("Listbox.Button"),u=_r("Listbox.Button"),s=ln(o.buttonRef,t),a=In(),p=A(N=>{switch(N.key){case ie.Space:case ie.Enter:case ie.ArrowDown:N.preventDefault(),u.openListbox(),a.nextFrame(()=>{o.value||u.goToOption(Ee.First)});break;case ie.ArrowUp:N.preventDefault(),u.openListbox(),a.nextFrame(()=>{o.value||u.goToOption(Ee.Last)});break}}),f=A(N=>{switch(N.key){case ie.Space:N.preventDefault();break}}),m=A(N=>{if(sh(N.currentTarget))return N.preventDefault();o.listboxState===0?(u.closeListbox(),a.nextFrame(()=>{var d;return(d=o.buttonRef.current)==null?void 0:d.focus({preventScroll:!0})})):(N.preventDefault(),u.openListbox())}),g=Nd(()=>{if(o.labelId)return[o.labelId,l].join(" ")},[o.labelId,l]),v=y.useMemo(()=>({open:o.listboxState===0,disabled:o.disabled,value:o.value}),[o]),x={ref:s,id:l,type:rh(e,o.buttonRef),"aria-haspopup":"listbox","aria-controls":(n=o.optionsRef.current)==null?void 0:n.id,"aria-expanded":o.disabled?void 0:o.listboxState===0,"aria-labelledby":g,disabled:o.disabled,onKeyDown:p,onKeyUp:f,onClick:m};return jt({ourProps:x,theirProps:i,slot:v,defaultTag:Sh,name:"Listbox.Button"})}),Ch="label",Nh=vt(function(e,t){let n=Xl(),{id:r=`headlessui-listbox-label-${n}`,...l}=e,i=Lr("Listbox.Label"),o=_r("Listbox.Label"),u=ln(i.labelRef,t);be(()=>o.registerLabel(r),[r]);let s=A(()=>{var p;return(p=i.buttonRef.current)==null?void 0:p.focus({preventScroll:!0})}),a=y.useMemo(()=>({open:i.listboxState===0,disabled:i.disabled}),[i]);return jt({ourProps:{ref:u,id:r,onClick:s},theirProps:l,slot:a,defaultTag:Ch,name:"Listbox.Label"})}),Ph="ul",Oh=$l.RenderStrategy|$l.Static,_h=vt(function(e,t){var n;let r=Xl(),{id:l=`headlessui-listbox-options-${r}`,...i}=e,o=Lr("Listbox.Options"),u=_r("Listbox.Options"),s=ln(o.optionsRef,t),a=In(),p=In(),f=Du(),m=(()=>f!==null?(f&_e.Open)===_e.Open:o.listboxState===0)();y.useEffect(()=>{var d;let c=o.optionsRef.current;!c||o.listboxState===0&&c!==((d=Pd(c))==null?void 0:d.activeElement)&&c.focus({preventScroll:!0})},[o.listboxState,o.optionsRef]);let g=A(d=>{switch(p.dispose(),d.key){case ie.Space:if(o.searchQuery!=="")return d.preventDefault(),d.stopPropagation(),u.search(d.key);case ie.Enter:if(d.preventDefault(),d.stopPropagation(),o.activeOptionIndex!==null){let{dataRef:c}=o.options[o.activeOptionIndex];u.onChange(c.current.value)}o.mode===0&&(u.closeListbox(),tn().nextFrame(()=>{var c;return(c=o.buttonRef.current)==null?void 0:c.focus({preventScroll:!0})}));break;case ce(o.orientation,{vertical:ie.ArrowDown,horizontal:ie.ArrowRight}):return d.preventDefault(),d.stopPropagation(),u.goToOption(Ee.Next);case ce(o.orientation,{vertical:ie.ArrowUp,horizontal:ie.ArrowLeft}):return d.preventDefault(),d.stopPropagation(),u.goToOption(Ee.Previous);case ie.Home:case ie.PageUp:return d.preventDefault(),d.stopPropagation(),u.goToOption(Ee.First);case ie.End:case ie.PageDown:return d.preventDefault(),d.stopPropagation(),u.goToOption(Ee.Last);case ie.Escape:return d.preventDefault(),d.stopPropagation(),u.closeListbox(),a.nextFrame(()=>{var c;return(c=o.buttonRef.current)==null?void 0:c.focus({preventScroll:!0})});case ie.Tab:d.preventDefault(),d.stopPropagation();break;default:d.key.length===1&&(u.search(d.key),p.setTimeout(()=>u.clearSearch(),350));break}}),v=Nd(()=>{var d,c,h;return(h=(d=o.labelRef.current)==null?void 0:d.id)!=null?h:(c=o.buttonRef.current)==null?void 0:c.id},[o.labelRef.current,o.buttonRef.current]),x=y.useMemo(()=>({open:o.listboxState===0}),[o]),N={"aria-activedescendant":o.activeOptionIndex===null||(n=o.options[o.activeOptionIndex])==null?void 0:n.id,"aria-multiselectable":o.mode===1?!0:void 0,"aria-labelledby":v,"aria-orientation":o.orientation,id:l,onKeyDown:g,role:"listbox",tabIndex:0,ref:s};return jt({ourProps:N,theirProps:i,slot:x,defaultTag:Ph,features:Oh,visible:m,name:"Listbox.Options"})}),Lh="li",Th=vt(function(e,t){let n=Xl(),{id:r=`headlessui-listbox-option-${n}`,disabled:l=!1,value:i,...o}=e,u=Lr("Listbox.Option"),s=_r("Listbox.Option"),a=u.activeOptionIndex!==null?u.options[u.activeOptionIndex].id===r:!1,p=u.isSelected(i),f=y.useRef(null),m=ct({disabled:l,value:i,domRef:f,get textValue(){var E,P;return(P=(E=f.current)==null?void 0:E.textContent)==null?void 0:P.toLowerCase()}}),g=ln(t,f);be(()=>{if(u.listboxState!==0||!a||u.activationTrigger===0)return;let E=tn();return E.requestAnimationFrame(()=>{var P,C;(C=(P=f.current)==null?void 0:P.scrollIntoView)==null||C.call(P,{block:"nearest"})}),E.dispose},[f,a,u.listboxState,u.activationTrigger,u.activeOptionIndex]),be(()=>s.registerOption(r,m),[m,r]);let v=A(E=>{if(l)return E.preventDefault();s.onChange(i),u.mode===0&&(s.closeListbox(),tn().nextFrame(()=>{var P;return(P=u.buttonRef.current)==null?void 0:P.focus({preventScroll:!0})}))}),x=A(()=>{if(l)return s.goToOption(Ee.Nothing);s.goToOption(Ee.Specific,r)}),N=ph(),d=A(E=>N.update(E)),c=A(E=>{!N.wasMoved(E)||l||a||s.goToOption(Ee.Specific,r,0)}),h=A(E=>{!N.wasMoved(E)||l||!a||s.goToOption(Ee.Nothing)}),w=y.useMemo(()=>({active:a,selected:p,disabled:l}),[a,p,l]);return jt({ourProps:{id:r,ref:g,role:"option",tabIndex:l===!0?void 0:-1,"aria-disabled":l===!0?!0:void 0,"aria-selected":p,disabled:void 0,onClick:v,onFocus:x,onPointerEnter:d,onMouseEnter:d,onPointerMove:c,onMouseMove:c,onPointerLeave:h,onMouseLeave:h},theirProps:o,slot:w,defaultTag:Lh,name:"Listbox.Option"})}),Yr=Object.assign(xh,{Button:Eh,Label:Nh,Options:_h,Option:Th});function Rh(e=0){let[t,n]=y.useState(e),r=y.useCallback(u=>n(s=>s|u),[t]),l=y.useCallback(u=>Boolean(t&u),[t]),i=y.useCallback(u=>n(s=>s&~u),[n]),o=y.useCallback(u=>n(s=>s^u),[n]);return{flags:t,addFlag:r,hasFlag:l,removeFlag:i,toggleFlag:o}}function zh(e){let t={called:!1};return(...n)=>{if(!t.called)return t.called=!0,e(...n)}}function $i(e,...t){e&&t.length>0&&e.classList.add(...t)}function Ii(e,...t){e&&t.length>0&&e.classList.remove(...t)}function $h(e,t){let n=tn();if(!e)return n.dispose;let{transitionDuration:r,transitionDelay:l}=getComputedStyle(e),[i,o]=[r,l].map(u=>{let[s=0]=u.split(",").filter(Boolean).map(a=>a.includes("ms")?parseFloat(a):parseFloat(a)*1e3).sort((a,p)=>p-a);return s});if(i+o!==0){let u=n.addEventListener(e,"transitionend",s=>{s.target===s.currentTarget&&(t(),u())})}else t();return n.add(()=>t()),n.dispose}function Ih(e,t,n,r){let l=n?"enter":"leave",i=tn(),o=r!==void 0?zh(r):()=>{};l==="enter"&&(e.removeAttribute("hidden"),e.style.display="");let u=ce(l,{enter:()=>t.enter,leave:()=>t.leave}),s=ce(l,{enter:()=>t.enterTo,leave:()=>t.leaveTo}),a=ce(l,{enter:()=>t.enterFrom,leave:()=>t.leaveFrom});return Ii(e,...t.enter,...t.enterTo,...t.enterFrom,...t.leave,...t.leaveFrom,...t.leaveTo,...t.entered),$i(e,...u,...a),i.nextFrame(()=>{Ii(e,...a),$i(e,...s),$h(e,()=>(Ii(e,...u),$i(e,...t.entered),o()))}),i.dispose}function Fh({container:e,direction:t,classes:n,onStart:r,onStop:l}){let i=Fd(),o=In(),u=ct(t);be(()=>{let s=tn();o.add(s.dispose);let a=e.current;if(a&&u.current!=="idle"&&i.current)return s.dispose(),r.current(u.current),s.add(Ih(a,n.current,u.current==="enter",()=>{s.dispose(),l.current(u.current)})),s.dispose},[t])}function Vt(e=""){return e.split(" ").filter(t=>t.trim().length>1)}let Zl=y.createContext(null);Zl.displayName="TransitionContext";var Dh=(e=>(e.Visible="visible",e.Hidden="hidden",e))(Dh||{});function Mh(){let e=y.useContext(Zl);if(e===null)throw new Error("A is used but it is missing a parent or .");return e}function jh(){let e=y.useContext(Jl);if(e===null)throw new Error("A is used but it is missing a parent or .");return e}let Jl=y.createContext(null);Jl.displayName="NestingContext";function ql(e){return"children"in e?ql(e.children):e.current.filter(({el:t})=>t.current!==null).filter(({state:t})=>t==="visible").length>0}function Dd(e,t){let n=ct(e),r=y.useRef([]),l=Fd(),i=In(),o=A((g,v=st.Hidden)=>{let x=r.current.findIndex(({el:N})=>N===g);x!==-1&&(ce(v,{[st.Unmount](){r.current.splice(x,1)},[st.Hidden](){r.current[x].state="hidden"}}),i.microTask(()=>{var N;!ql(r)&&l.current&&((N=n.current)==null||N.call(n))}))}),u=A(g=>{let v=r.current.find(({el:x})=>x===g);return v?v.state!=="visible"&&(v.state="visible"):r.current.push({el:g,state:"visible"}),()=>o(g,st.Unmount)}),s=y.useRef([]),a=y.useRef(Promise.resolve()),p=y.useRef({enter:[],leave:[],idle:[]}),f=A((g,v,x)=>{s.current.splice(0),t&&(t.chains.current[v]=t.chains.current[v].filter(([N])=>N!==g)),t?.chains.current[v].push([g,new Promise(N=>{s.current.push(N)})]),t?.chains.current[v].push([g,new Promise(N=>{Promise.all(p.current[v].map(([d,c])=>c)).then(()=>N())})]),v==="enter"?a.current=a.current.then(()=>t?.wait.current).then(()=>x(v)):x(v)}),m=A((g,v,x)=>{Promise.all(p.current[v].splice(0).map(([N,d])=>d)).then(()=>{var N;(N=s.current.shift())==null||N()}).then(()=>x(v))});return y.useMemo(()=>({children:r,register:u,unregister:o,onStart:f,onStop:m,wait:a,chains:p}),[u,o,r,f,m,p,a])}function Ah(){}let Uh=["beforeEnter","afterEnter","beforeLeave","afterLeave"];function oa(e){var t;let n={};for(let r of Uh)n[r]=(t=e[r])!=null?t:Ah;return n}function bh(e){let t=y.useRef(oa(e));return y.useEffect(()=>{t.current=oa(e)},[e]),t}let Vh="div",Md=$l.RenderStrategy,jd=vt(function(e,t){let{beforeEnter:n,afterEnter:r,beforeLeave:l,afterLeave:i,enter:o,enterFrom:u,enterTo:s,entered:a,leave:p,leaveFrom:f,leaveTo:m,...g}=e,v=y.useRef(null),x=ln(v,t),N=g.unmount?st.Unmount:st.Hidden,{show:d,appear:c,initial:h}=Mh(),[w,E]=y.useState(d?"visible":"hidden"),P=jh(),{register:C,unregister:T}=P,b=y.useRef(null);y.useEffect(()=>C(v),[C,v]),y.useEffect(()=>{if(N===st.Hidden&&v.current){if(d&&w!=="visible"){E("visible");return}return ce(w,{hidden:()=>T(v),visible:()=>C(v)})}},[w,v,C,T,d,N]);let $=ct({enter:Vt(o),enterFrom:Vt(u),enterTo:Vt(s),entered:Vt(a),leave:Vt(p),leaveFrom:Vt(f),leaveTo:Vt(m)}),le=bh({beforeEnter:n,afterEnter:r,beforeLeave:l,afterLeave:i}),Ve=$u();y.useEffect(()=>{if(Ve&&w==="visible"&&v.current===null)throw new Error("Did you forget to passthrough the `ref` to the actual DOM node?")},[v,w,Ve]);let He=h&&!c,on=(()=>!Ve||He||b.current===d?"idle":d?"enter":"leave")(),rt=Rh(0),Be=A(F=>ce(F,{enter:()=>{rt.addFlag(_e.Opening),le.current.beforeEnter()},leave:()=>{rt.addFlag(_e.Closing),le.current.beforeLeave()},idle:()=>{}})),At=A(F=>ce(F,{enter:()=>{rt.removeFlag(_e.Opening),le.current.afterEnter()},leave:()=>{rt.removeFlag(_e.Closing),le.current.afterLeave()},idle:()=>{}})),O=Dd(()=>{E("hidden"),T(v)},P);Fh({container:v,classes:$,direction:on,onStart:ct(F=>{O.onStart(v,F,Be)}),onStop:ct(F=>{O.onStop(v,F,At),F==="leave"&&!ql(O)&&(E("hidden"),T(v))})}),y.useEffect(()=>{!He||(N===st.Hidden?b.current=null:b.current=d)},[d,He,w]);let R=g,z={ref:x};return c&&d&&zt.isServer&&(R={...R,className:_d(g.className,...$.current.enter,...$.current.enterFrom)}),ue.createElement(Jl.Provider,{value:O},ue.createElement(Id,{value:ce(w,{visible:_e.Open,hidden:_e.Closed})|rt.flags},jt({ourProps:z,theirProps:R,defaultTag:Vh,features:Md,visible:w==="visible",name:"Transition.Child"})))}),Do=vt(function(e,t){let{show:n,appear:r=!1,unmount:l,...i}=e,o=y.useRef(null),u=ln(o,t);$u();let s=Du();if(n===void 0&&s!==null&&(n=(s&_e.Open)===_e.Open),![!0,!1].includes(n))throw new Error("A is used but it is missing a `show={true | false}` prop.");let[a,p]=y.useState(n?"visible":"hidden"),f=Dd(()=>{p("hidden")}),[m,g]=y.useState(!0),v=y.useRef([n]);be(()=>{m!==!1&&v.current[v.current.length-1]!==n&&(v.current.push(n),g(!1))},[v,n]);let x=y.useMemo(()=>({show:n,appear:r,initial:m}),[n,r,m]);y.useEffect(()=>{if(n)p("visible");else if(!ql(f))p("hidden");else{let d=o.current;if(!d)return;let c=d.getBoundingClientRect();c.x===0&&c.y===0&&c.width===0&&c.height===0&&p("hidden")}},[n,f]);let N={unmount:l};return ue.createElement(Jl.Provider,{value:f},ue.createElement(Zl.Provider,{value:x},jt({ourProps:{...N,as:y.Fragment,children:ue.createElement(jd,{ref:u,...N,...i})},theirProps:{},defaultTag:y.Fragment,features:Md,visible:a==="visible",name:"Transition"})))}),Hh=vt(function(e,t){let n=y.useContext(Zl)!==null,r=Du()!==null;return ue.createElement(ue.Fragment,null,!n&&r?ue.createElement(Do,{ref:t,...e}):ue.createElement(jd,{ref:t,...e}))}),Bh=Object.assign(Do,{Child:Hh,Root:Do});function Wh({options:e,selectedOption:t,setSelected:n,label:r,size:l="large",testId:i,disabled:o}){let u="";switch(l){case"small":u="py-1 bg-white dark:bg-slate-900 hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-500 dark:text-slate-400 dark:ring-offset-slate-900";break;case"medium":u="py-2 bg-white text-slate-400 hover:bg-slate-100";break;case"large":u="!py-2 bg-slate-white text-slate-300 hover:bg-slate-100 m-1 !rounded-lg focus:!ring-offset-0";break}return k(Yr,{value:t,onChange:n,children:({open:s})=>L("div",{"data-test":i,className:W("relative",o&&"opacity-60 cursor-not-allowed"),children:[r&&k(Cd,{className:"w-full inline-block pb-px",children:r}),L("div",{className:"relative",children:[k("div",{className:"rounded-xl w-full",children:L("div",{className:"relative z-0 inline-flex rounded-xl w-full border border-slate-200 dark:border-slate-700",children:[k("div",{className:"relative flex flex-grow items-center bg-white dark:bg-slate-900 pl-3 pr-4 border border-transparent rounded-l-xl text-slate-700 dark:text-slate-300",children:k("p",{className:"ml-2.5 font-medium",children:(e.find(a=>a.value===t)??e[0])?.display??"make a selection..."})}),L(Yr.Button,{className:W("relative inline-flex items-center px-4 rounded-l-none rounded-r-xl text-sm font-medium focus:outline-none focus:z-10 focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-50 focus:ring-indigo-500 transition-[outline,background-color] duration-100",u,o&&"cursor-not-allowed pointer-events-none"),children:[k("span",{className:"sr-only",children:"Change published status"}),k("i",{className:`fa fa-chevron-down text-xl text-opacity-90 transition-transform duration-100 ${s?"-rotate-180":"rotate-0"}`,"aria-hidden":"true"})]})]})}),k(Bh,{show:s,as:y.Fragment,leave:"transition-[opacity,transform] ease-in duration-100",leaveFrom:"opacity-100",leaveTo:"opacity-0 -translate-y-1",enter:"transition-[opacity,transform] ease-in duration-100",enterFrom:"opacity-0 -translate-y-1",enterTo:"opacity-100",children:k(Yr.Options,{className:"origin-top-right absolute z-20 right-0 mt-2 p-2 w-72 rounded-xl shadow-lg overflow-hidden bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 focus:outline-none flex flex-col",children:e.map(({value:a,display:p})=>k(Yr.Option,{className:({active:f})=>W(f?"bg-violet-100 text-slate-900 dark:text-slate-200":"text-slate-900 dark:text-slate-300","cursor-pointer select-none relative rounded-xl transition-[background-color] duration-75",l==="small"?"py-2 px-3":"p-3.5 text-md",l==="small"&&f&&"bg-slate-50 dark:bg-slate-700/50"),value:a,children:({selected:f})=>k("div",{className:"flex flex-col",children:L("div",{className:"flex justify-between",children:[k("p",{className:f?"font-semibold":"font-normal",children:p}),f?k("span",{className:"text-violet-700",children:k("i",{className:"fa fa-check h-5 w-5","aria-hidden":"true"})}):null]})})},a))})})]})]})})}const Qh=({type:e,className:t,children:n,size:r})=>{let l="";switch(e){case"green":case"ok":l="bg-green-50 dark:bg-green-500/10 border-green-200 dark:border-green-500/50 text-green-600 dark:text-green-300";break;case"red":case"error":l="bg-red-50 dark:bg-red-500/10 border-red-200 dark:border-red-500/50 text-red-600 dark:text-red-300";break;case"yellow":case"warning":l="bg-yellow-50 dark:bg-yellow-500/10 border-yellow-200 dark:border-yellow-500/50 text-yellow-600 dark:text-yellow-300";break;case"blue":case"info":l="bg-blue-50 dark:bg-blue-500/10 border-blue-200 dark:border-blue-500/50 text-blue-600 dark:text-blue-300";break}return k("div",{className:W("max-w-fit border rounded-full flex justify-center items-center",{"text-xs px-[12px] py-[2px]":r==="small","text-base px-6 py-0.5":r==="large","text-sm px-[14px] py-[2.5px]":r==="medium"||!r},l,t),children:n})},Kh=({short:e,onRecheck:t,onDisconnect:n})=>{const[r,l]=y.useState(!1),[i,o]=y.useState(!1);return k("div",{className:W("min-h-screen flex flex-col justify-center items-center bg-white dark:bg-slate-900",e?"p-4":"p-8"),children:L("div",{className:W("border border-red-100 dark:border-red-500/40 rounded-2xl h-full flex flex-col bg-red-50/50 dark:bg-red-500/20",e?"p-6":"p-8"),children:[L("div",{className:"flex-grow flex flex-col space-y-4 text-slate-900 dark:text-slate-100",children:[L("h1",{className:"text-2xl font-medium",children:["Your Gertrude account is ",k("strong",{children:"no longer active."})]}),!e&&k("p",{children:"The internet filter will continue protecting this computer according to the rules set before the account went inactive, but no changes or suspensions can be made until the account is restored."}),L("p",{children:["To ",k("strong",{children:"restore the account,"})," login to the Gertrude parent site and resolve the payment issue, then click the ",k("strong",{children:"Recheck"})," ","button below."]}),!e&&L("p",{children:["If you no longer wish to use Gertrude, click the ",k("strong",{children:"Disconnect"})," ","button below, then uninstall the app."]}),L("p",{children:["Contact us at at"," ",k("a",{href:"https://gertrude.app/contact",className:"font-semibold text-slate-800 dark:text-slate-200 border-b-2 pb-1 border-transparent dark:border-transparent hover:border-slate-600 dark:hover:border-slate-100 hover:pb-0.5 duration-200 transition-[padding-bottom,border-color]",children:"https://gertrude.app/contact"})," ","to get help."]})]}),L("div",{className:"flex items-center mt-6 space-x-4 bg-white dark:bg-slate-900 p-4 rounded-xl self-stretch justify-between border border-red-100 dark:border-red-500/50",children:[L(q,{type:"button",onClick:()=>{l(!0),setTimeout(()=>l(!1),3e3),t()},color:"secondary",disabled:r,children:[k("i",{className:W("fa-solid fa-sync mr-3",r&&"animate-spin")}),r?"Rechecking...":"Recheck"]}),L(q,{type:"button",onClick:()=>{o(!0),setTimeout(()=>o(!1),6e3),n()},color:"warning",disabled:i,children:[k("i",{className:W("fa-solid mr-3",i?"fa-sync animate-spin":"fa-plug")}),i?"Disconnecting...":"Disconnect"]})]})]})})},Gh=({screen:e,setScreen:t})=>{const[n,r]=y.useState(0);return L("nav",{className:W("border-slate-200 dark:border-slate-800 border-r p-2 font-bold flex flex-col items-stretch space-y-1 bg-white dark:bg-slate-900 fixed h-full z-20 top-0"),children:[k(Xr,{isActive:e==="healthCheck",onClick:()=>t("healthCheck"),icon:"heart-pulse"}),k(Xr,{isActive:e==="actions",onClick:()=>t("actions"),icon:"arrow-pointer"}),k(Xr,{isActive:e==="exemptUsers",onClick:()=>t("exemptUsers"),icon:"users"}),k(Xr,{isActive:e==="advanced",onClick:()=>{n<10?r(n+1):t("advanced")},icon:"flask",className:n<10?"cursor-default opacity-0":""})]})},Xr=({onClick:e,isActive:t,className:n,icon:r})=>k("button",{onClick:e,className:W("transition-colors duration-100 w-12 h-12 flex justify-center items-center rounded-lg",t?"bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400":"text-slate-500 hover:bg-slate-50 dark:hover:bg-slate-800/50 hover:text-slate-600 dark:hover:text-slate-400",n),children:k("i",{className:`fa-solid fa-${r} text-2xl`})}),Yh=({filterRunning:e,installedAppVersion:t,availableAppUpdate:n,dangerZoneModal:r,userName:l,emit:i,quitting:o,releaseChannel:u,dispatch:s})=>{const a=Xh(n);return L("div",{className:"p-4 h-full flex flex-col justify-between relative",children:[k("div",{className:W("w-full h-full left-0 top-0 bg-slate-100 dark:bg-slate-900 z-10 transition-[backdrop-filter,background-color] duration-300 flex justify-center items-center fixed",r!=="hidden"?"bg-opacity-30 dark:bg-opacity-90 dark:backdrop-blur-sm backdrop-blur-md pointer-events-auto":"bg-opacity-0 dark:bg-opacity-0 backdrop-blur-none pointer-events-none"),onClick:()=>s({type:"dangerZoneModalDismissed"}),children:L("div",{className:W("bg-white dark:bg-slate-800 shadow-lg shadow-slate-300/50 dark:shadow-black/20 rounded-2xl transition-[transform,opacity] duration-300",r!=="hidden"?"pointer-events-auto":"pointer-events-none scale-75 opacity-0"),onClick:p=>p.stopPropagation(),children:[L("div",{className:"p-8",children:[L("h3",{className:"font-bold text-xl text-slate-900 dark:text-slate-200 ",children:["Are you sure you want to",r==="quitApp"?" quit Gertrude?":" stop the filter?"]}),k("p",{className:"max-w-md text-sm text-slate-500 dark:text-slate-400 mt-4",children:r==="quitApp"?"Quitting the app stops all screenshot and keystroke monitoring. This is usually only necessary when uninstalling or troubleshooting.":'Stopping the filter gives all users on this computer unrestricted internet access. If you want to temporarily suspend the filter, use the "Suspend filter" button in the main menubar dropdown instead.'})]}),L("div",{className:"p-4 bg-slate-50 dark:bg-slate-900/50 rounded-b-2xl flex justify-between space-x-4",children:[k(q,{type:"button",onClick:()=>s({type:"dangerZoneModalDismissed"}),color:"tertiary",className:"flex-grow",children:"Cancel"}),k(q,{type:"button",onClick:()=>i({case:r==="quitApp"?"confirmQuitAppClicked":"confirmStopFilterClicked"}),color:"warning",className:"flex-grow",disabled:r==="quitApp"&&o,children:o?"Quitting...":`I understand, ${r==="quitApp"?"quit the app":"stop the filter"}`})]})]})}),L("div",{className:"flex flex-col flex-grow",children:[L("div",{className:"border border-slate-200 dark:border-slate-800 rounded-2xl relative flex flex-col justify-between mb-3.5",children:[k(Qh,{type:a.badgeColor,className:"absolute right-2 top-2 w-36 !max-w-none",children:a.badgeText}),L("div",{className:"p-4 pt-3",children:[L("h2",{className:"text-lg font-semibold text-slate-600 dark:text-slate-300",children:["Currently running Gertrude version"," ",k("span",{className:"font-bold *font-mono text-violet-700 dark:text-violet-400",children:t})]}),k("p",{className:"text-slate-500 mt-2",children:a.versionMessage})]}),L("div",{className:"p-3 bg-slate-50 dark:bg-slate-800/50 rounded-b-2xl flex justify-end items-center",children:[L("p",{className:W("text-slate-500 opacity-80 italic ml-3 flex-grow",u==="stable"&&"hidden"),children:[k("i",{className:"fas fa-flask mr-2"}),"Release channel: ",k("b",{children:u})]}),L(q,{type:"button",size:"small",onClick:()=>i({case:"updateAppNowClicked"}),color:"tertiary",disabled:!n,children:[k("i",{className:"fas fa-sync-alt mr-2"}),"Update now"]})]})]}),l!==void 0&&L("div",{className:"border border-slate-200 dark:border-slate-800 rounded-2xl relative flex flex-col justify-between",children:[L("div",{className:"p-4 pt-3",children:[L("h2",{className:"text-lg font-semibold text-slate-600 dark:text-slate-300",children:["Connected to child:"," ",k("span",{className:"font-bold *font-mono text-violet-700 dark:text-violet-400",children:l})]}),k("p",{className:"text-slate-500 mt-2",children:"Disconnect if you want to connect a different child."})]}),k("div",{className:"p-3 bg-slate-50 dark:bg-slate-800/50 rounded-b-2xl flex justify-end",children:L(q,{type:"button",size:"small",onClick:()=>i({case:"disconnectUserClicked"}),color:"tertiary",children:[k("i",{className:"fa fa-scissors mr-2"}),"Disconnect"]})})]})]}),L("div",{className:"border border-red-200/70 dark:border-red-500/20 rounded-2xl bg-red-50/20 dark:bg-red-500/5",children:[L("div",{className:"p-4 pt-3",children:[k("h2",{className:"text-xl font-bold text-red-700 dark:text-red-400",children:"Danger zone"}),k("p",{className:"mt-2 text-red-700/70 dark:text-red-400/50",children:"These actions disable Gertrude‘s protections. Use only for troubleshooting or uninstalling."})]}),L("div",{className:"flex justify-end space-x-4 bg-red-50/30 dark:bg-red-500/5 rounded-b-2xl p-3",children:[e&&k(q,{type:"button",size:"small",onClick:()=>s({type:"dangerZoneStopFilterClicked"}),color:"warning",children:"Stop filter"}),k(q,{type:"button",size:"small",onClick:()=>s({type:"dangerZoneQuitAppClicked"}),color:"warning",children:"Quit app"})]})]})]})};function Xh(e){return e?e.required?{badgeColor:"red",badgeText:"Update required",versionMessage:L(ma,{children:["Update to required version ",k("b",{children:e.semver})," as soon as possible."]})}:{badgeColor:"yellow",badgeText:"Update available",versionMessage:`Gertrude version ${e.semver} is available for download.`}:{badgeColor:"green",badgeText:"Up to date",versionMessage:"We'll let you know when the next update is available."}}const Zh=e=>{let t,n;switch(e.state){case"ok":t="bg-green-500",n="fa-solid fa-check translate-x-[0.5px]";break;case"fail":t="bg-red-500",n="fa-solid fa-times translate-x-[0.5px]";break;case"warn":t="bg-yellow-400",n="fa-solid fa-minus";break;case"checking":t="bg-purple-500 dark:bg-purple-800",n="fa-solid fa-sync animate-spin dark:text-slate-100";break;case"unexpected":t="bg-gray-500/90",n="fa-solid fa-exclamation translate-x-[0.5px]";break}const r=Jh(e),l=qh(e);return L("div",{className:"flex items-center p-2 rounded-xl bg-slate-50 dark:bg-slate-800/30",children:[k("div",{className:W("w-6 h-6 rounded-full flex justify-center items-center",t),children:k("i",{className:W("text-white dark:text-slate-900",n)})}),L("div",{className:"flex-grow ml-4",children:[k("h3",{className:"font-medium text-slate-800 dark:text-slate-200",children:e.title}),l&&k("p",{className:"text-slate-500 dark:text-slate-400",dangerouslySetInnerHTML:{__html:l}})]}),r&&L(q,{type:"button",onClick:()=>e.emit(r.action),color:"tertiary",size:"small",children:[k("i",{className:`fa-solid fa-${r.icon} mr-2`}),r.label]})]})};function Jh(e){if(e.state==="warn"||e.state==="fail")return e.button}function qh(e){switch(e.state){case"ok":return e.message;case"fail":return e.message;case"warn":return e.message;case"unexpected":return e.message??"Unexpected check error, please try again";default:return}}function em(e){return Object.entries(e)}function tm(e){return e!=null}class nm{constructor(t,n,r,l){this.data=t,this.installedAppVersion=n,this.screenshotMonitoringEnabled=r,this.keystrokeMonitoringEnabled=l}get items(){return[this.appVersion,...this.filterItems,this.screenRecordingPermission,this.keystrokeRecordingPermission,this.notificationsPermission,this.macOsUserType,this.accountStatus].filter(tm)}get failingChecksCount(){return this.items.filter(({state:t})=>t==="fail").length}get isChecking(){return this.items.some(({state:t})=>t==="checking")}get appVersion(){const{latestAppVersion:t}=this.data;return t===void 0?{title:"App Version",state:"checking"}:Kr(t)?{title:"App Version",state:"unexpected",message:g0(t)}:this.installedAppVersion===t.value||this.installedAppVersion>t.value?{title:"App Version",state:"ok",message:`You're up to date (${this.installedAppVersion})`}:{title:"App Version",state:"warn",message:`Update available (${t.value})`,button:{icon:"sync",label:"Update",action:"upgradeAppClicked"}}}get screenRecordingPermission(){if(this.screenshotMonitoringEnabled)return this.data.screenRecordingPermissionOk===void 0?{title:"Screen recording permission",state:"checking"}:this.data.screenRecordingPermissionOk?{title:"Screen recording permission",state:"ok"}:{title:"Screen recording permission",state:"fail",message:"Gertrude can't take screenshots until you give permission",button:{icon:"cog",label:"Fix permission",action:"fixScreenRecordingPermissionClicked"}}}get keystrokeRecordingPermission(){if(this.keystrokeMonitoringEnabled)return this.data.keystrokeRecordingPermissionOk===void 0?{title:"Keystroke recording permission",state:"checking"}:this.data.keystrokeRecordingPermissionOk?{title:"Keystroke recording permission",state:"ok"}:{title:"Keystroke recording permission",state:"fail",message:"Gertrude can't monitor keystrokes until you give permission",button:{icon:"cog",label:"Fix permission",action:"fixKeystrokeRecordingPermissionClicked"}}}get macOsUserType(){return this.data.macOsUserType===void 0?{title:"macOS user account type",state:"checking"}:Kr(this.data.macOsUserType)?{title:"macOS user account type",state:"unexpected"}:this.data.macOsUserType.value!=="standard"?{title:"Mac user has admin privileges",state:"fail",message:"Admin users can disable Gertrude if they have the password",button:{icon:"user",label:"Remove admin privilege",action:"removeUserAdminPrivilegeClicked"}}:{title:"macOS user account type",state:"ok"}}get notificationsPermission(){switch(this.data.notificationsSetting){case"alert":return{title:"Notification settings",state:"ok"};case"banner":return{title:"Notification settings",state:"warn",message:'Set to "banner", recommended setting is "alert"',button:{icon:"cog",label:"Fix setting",action:"fixNotificationPermissionClicked"}};case"none":return{title:"Notification settings",state:"fail",message:"Notifications disabled, child will miss critical updates",button:{icon:"cog",label:"Fix setting",action:"fixNotificationPermissionClicked"}};default:return{title:"Notification settings",state:"checking"}}}get accountStatus(){if(this.data.accountStatus===void 0)return{title:"Gertrude account status",state:"checking"};if(Kr(this.data.accountStatus))return{title:"Gertrude account status",state:"unexpected"};switch(this.data.accountStatus.value){case"active":return{title:"Gertrude account status",state:"ok"};case"needsAttention":return{title:"Gertrude account status",state:"warn",message:"Needs attention: log in to the Gertrude parents website for more details"};default:return{title:"Gertrude account status",state:"fail",message:"Log in to the Gertrude parents website to resolve"}}}get filterItems(){const{filterStatus:t,latestAppVersion:n}=this.data;if(t===void 0||n===void 0||t.case==="installing"||t.case==="communicationBroken"&&t.repairing)return[{title:"Filter status",state:"checking"}];if(t.case==="disabled")return[{title:"Filter status",state:"warn",message:"Filter has been disabled",button:{icon:"cog",label:"Enable filter",action:"enableFilterClicked"}}];if(t.case==="notInstalled")return[{title:"Filter status",state:"warn",message:"Filter has not been installed",button:{icon:"cog",label:"Install filter",action:"installFilterClicked"}}];if(t.case==="installTimeout")return[{title:"Filter status",state:"unexpected",message:"Installation did not complete, try again"}];if(t.case==="unexpected")return[{title:"Filter status",state:"unexpected",message:"Unexpected error: try rebooting the computer"}];if(t.case==="communicationBroken")return[{title:"Filter to app communication broken",state:"fail",message:"If repair and recheck fails, restart the computer to resolve",button:{icon:"sync",label:"Attempt repair",action:"repairFilterCommunicationClicked"}}];const r=[],{version:l,numUserKeys:i}=t;return this.shouldShowFilterOutOfDateItem(n,l)&&r.push({title:"Filter version",state:"fail",message:`Filter version out of date (${l})`,button:{icon:"sync",label:"Reinstall filter",action:"repairOutOfDateFilterClicked"}}),i>0?r.push({title:"Filter rules",state:"ok",message:`Looks good, ${i} keys loaded`}):r.push({title:"Filter rules",state:"warn",message:"No keys loaded, try refreshing rules",button:{icon:"sync",label:"Refresh rules",action:"zeroKeysRefreshRulesClicked"}}),r}shouldShowFilterOutOfDateItem(t,n){return Kr(t)?n!==this.installedAppVersion:this.installedAppVersion!==t.value?!1:n!==this.installedAppVersion}}const rm=({installedAppVersion:e,screenshotMonitoringEnabled:t,keystrokeMonitoringEnabled:n,emit:r,...l})=>{const i=new nm(l,e,t,n);return L("div",{className:"h-full overflow-y-auto relative",children:[L("header",{className:"flex items-center justify-between border-b p-4 border-slate-200 dark:border-slate-800 sticky bg-white dark:bg-slate-900 top-0 z-10",children:[L("div",{children:[k("h2",{className:"text-2xl font-bold text-slate-800 dark:text-slate-100",children:"Health check"}),k("span",{className:W("text-slate-600 dark:text-slate-400",i.isChecking&&"italic opacity-50"),children:i.isChecking?"Checking...":i.failingChecksCount?`${i.failingChecksCount} ${B0("failing check",i.failingChecksCount)}!`:"Everything looks good!"})]}),L(q,{type:"button",onClick:()=>r("recheckClicked"),disabled:i.isChecking,color:"secondary",size:"small",children:[k("i",{className:W("fa-solid fa-sync mr-2",i.isChecking&&"animate-spin")}),i.isChecking?"Checking...":"Recheck"]})]}),k("ul",{className:"flex flex-col space-y-2 p-4",children:i.items.map(o=>k(Zh,{...o,emit:r},o.title))})]})},lm=({title:e,children:t,button:n,className:r})=>L("div",{className:W("flex flex-col items-center justify-center p-4 rounded-2xl border border-red-200 dark:border-red-700/50 bg-red-50/30 dark:bg-red-600/5 flex-grow",r),children:[k("span",{className:"font-bold text-lg text-slate-700 dark:text-white/80 mb-2",children:e}),t&&k("span",{className:"text-red-500 mb-4 dark:text-red-400",children:t}),n&&L(q,{type:"button",onClick:n.action,color:"warning",size:"small",children:[k("i",{className:`fa-solid ${n.icon} mr-2`}),n.text]})]}),im=({users:e,emit:t})=>e?e.case==="error"?k("div",{className:"p-6",children:k(lm,{title:"Unexpected error",button:{text:"Check health",icon:"fa-heart-pulse",action:()=>t({case:"gotoScreenClicked",screen:"healthCheck"})},children:"Check health, or contact support if the problem persists."})}):L("div",{className:"flex flex-col h-full",children:[k("header",{className:"flex items-center justify-between border-b p-4 border-slate-200 sticky bg-white dark:border-slate-800 dark:bg-slate-900 top-0",children:k("h2",{className:"text-2xl font-bold text-slate-800 dark:text-slate-100",children:"Exempt users"})}),L("main",{className:"p-4 flex-grow flex flex-col relative",children:[L("div",{className:"mr-4",children:[L("p",{className:"text-slate-500 dark:text-slate-400",children:["Gertrude's network filter has to make decisions about whether to allow or deny network requests from every user on this computer. For maximum internet safety, it defaults to blocking all requests for users that it doesn't have rules for. If this computer has another user or users who should have unrestricted internet access (like a parent's admin account on a shared computer), you can make that user"," ",k("strong",{className:"text-slate-700 dark:text-slate-200",children:"exempt from filtering"})," ","by selecting the user name below."]}),L("p",{className:"text-slate-500 dark:text-slate-400 mt-4",children:[k("strong",{className:"text-slate-700 dark:text-slate-200",children:"Please note:"})," ","any user that is exempt from filtering should have a password enabled that is unknown to any individual subject to filtering, or else they would be able to log in to that user at any time and also have unrestricted internet access."]})]}),k("ul",{className:"mt-4 space-y-2 flex-grow",children:e.value.map(n=>k(om,{name:n.name,isExempt:n.isExempt,onToggle:()=>t({case:"setUserExemption",userId:n.id,enabled:!n.isExempt})},n.id))}),k("div",{className:"flex justify-end mt-4",children:L(q,{type:"button",onClick:()=>t({case:"administrateOSUserAccountsClicked"}),color:"secondary",size:"medium",className:"",children:["Administrate user accounts",k("i",{className:"fa-solid fa-arrow-right ml-2"})]})})]})]}):null,om=({name:e,isExempt:t,onToggle:n})=>L("div",{onClick:n,className:W("flex items-center justify-start rounded-xl p-2 pl-4",t&&"bg-red-50 dark:bg-red-500/10"),children:[k("button",{className:W("w-5 h-5 rounded-full border-slate-300 dark:border-slate-700 border mr-4 flex justify-center items-center hover:scale-105 transition-[transform,border-color,border,background-color] duration-100",t&&"bg-red-500 !border-red-500 dark:border-red-500"),children:k("i",{className:"fa-solid fa-check text-white dark:text-slate-900 text-xs"})}),L("div",{className:"flex items-center space-x-2 grow",children:[k("h3",{className:"font-bold dark:text-white grow",children:e}),k("span",{className:"text-red-500 dark:text-red-400 pr-2",children:t?"exempt from filtering - unrestricted internet access":""})]})]}),um=({emit:e,pairqlEndpointDefault:t,pairqlEndpointOverride:n,websocketEndpointDefault:r,websocketEndpointOverride:l,appcastEndpointDefault:i,appcastEndpointOverride:o,appVersions:u})=>{const[s,a]=y.useState(n??""),[p,f]=y.useState(l??""),[m,g]=y.useState(o??""),[v,x]=y.useState(""),N=u?em(u).map(([d,c])=>({display:c,value:d})).sort((d,c)=>c.value.localeCompare(d.value)):null;return L("div",{className:"flex flex-col items-stretch h-full p-6 space-y-5",children:[L("div",{className:"pb-8 flex justify-between items-start",children:[N?L("div",{className:"flex space-x-2 items-end",children:[k(q,{size:"small",disabled:v==="",onClick:()=>e({case:"forceUpdateToSpecificVersionClicked",version:v}),type:"button",color:"tertiary",children:"Force update to:"}),k(Wh,{size:"small",options:[{display:"choose version...",value:""},...N],selectedOption:v,setSelected:x})]}):k("div",{className:"text-gray-400 italic",children:"Loading app versions..."}),k(q,{className:"h-12",onClick:()=>e({case:"deleteAllDeviceStorageClicked"}),type:"button",color:"warning",children:"Purge all device storage"})]}),L("div",{className:"flex items-end gap-x-2",children:[k(_i,{label:"API PairQL endpoint override:",type:"url",value:s,placeholder:t,setValue:a}),k(q,{className:"h-12",disabled:s.trim()==="",onClick:()=>e({case:"pairqlEndpointSet",url:s.trim()}),type:"button",color:"secondary",children:"Set"}),k(q,{className:"h-12",disabled:n===void 0,onClick:()=>e({case:"pairqlEndpointSet",url:void 0}),type:"button",color:"secondary",children:"Clear"})]}),L("div",{className:"flex items-end gap-x-2",children:[k(_i,{label:"Websocket endpoint override:",type:"url",value:p,placeholder:r,setValue:f}),k(q,{className:"h-12",disabled:p.trim()==="",onClick:()=>e({case:"websocketEndpointSet",url:p.trim()}),type:"button",color:"secondary",children:"Set"}),k(q,{className:"h-12",disabled:l===void 0,onClick:()=>e({case:"websocketEndpointSet",url:void 0}),type:"button",color:"secondary",children:"Clear"})]}),L("div",{className:"flex items-end gap-x-2",children:[k(_i,{label:"Sparkle Appcast endpoint override:",type:"url",value:m,placeholder:i,setValue:g}),k(q,{className:"h-12",disabled:m.trim()==="",onClick:()=>e({case:"appcastEndpointSet",url:m.trim()}),type:"button",color:"secondary",children:"Set"}),k(q,{className:"h-12",disabled:o===void 0,onClick:()=>e({case:"appcastEndpointSet",url:void 0}),type:"button",color:"secondary",children:"Clear"})]})]})};class sm extends y0{appState(){return{windowOpen:!0,screen:"healthCheck",filterState:{case:"off"},installedAppVersion:"0.0.0",healthCheck:{},releaseChannel:"stable",quitting:!1}}viewState(){return{filterSuspensionDurationInSeconds:String(60*5),dangerZoneModal:"hidden"}}initializer(){return{...this.appState(),...this.viewState()}}reducer(t,n){switch(n.type){case"filterSuspensionDurationInSecondsChanged":return{...t,filterSuspensionDurationInSeconds:n.value};case"receivedUpdatedAppState":return{...t,...n.appState};case"dangerZoneStopFilterClicked":return{...t,dangerZoneModal:"stopFilter"};case"dangerZoneQuitAppClicked":return{...t,dangerZoneModal:"quitApp"};case"dangerZoneModalDismissed":return{...t,dangerZoneModal:"hidden"};case"appEventEmitted":return n.event.case==="confirmStopFilterClicked"?{...t,dangerZoneModal:"hidden"}:t}}}const am=new sm,cm=({healthCheck:e,filterState:t,user:n,installedAppVersion:r,availableAppUpdate:l,releaseChannel:i,exemptableUsers:o,screen:u,advanced:s,quitting:a,dangerZoneModal:p,emit:f,dispatch:m})=>{let g;switch(u){case"healthCheck":g=k(rm,{...e,installedAppVersion:r,screenshotMonitoringEnabled:n?.keystrokeMonitoringEnabled??!1,keystrokeMonitoringEnabled:n?.screenshotMonitoringEnabled??!1,emit:v=>f({case:"healthCheck",action:v})});break;case"actions":g=k(Yh,{releaseChannel:i,emit:f,dispatch:m,filterRunning:t.case==="on",installedAppVersion:r,availableAppUpdate:l,dangerZoneModal:p,userName:n?.name,quitting:a});break;case"exemptUsers":g=k(im,{emit:f,users:o});break;case"advanced":g=s?k(um,{...s,emit:v=>f({case:"advanced",action:v})}):k(ma,{children:"Loading..."});break}return v0(e.accountStatus)==="inactive"?k(Kh,{onRecheck:()=>f({case:"inactiveAccountRecheckClicked"}),onDisconnect:()=>f({case:"inactiveAccountDisconnectAppClicked"})}):k("div",{className:"flex flex-col h-screen",children:L("div",{className:W("flex flex-grow relative"),children:[k(Gh,{screen:u,setScreen:v=>f({case:"gotoScreenClicked",screen:v})}),k("main",{className:"flex-grow bg-white dark:bg-slate-900 ml-16",children:g})]})})},dm=k0(am,cm);Di.createRoot(document.getElementById("app")).render(k(dm,{})); diff --git a/macapp/Xcode/Gertrude/WebViews/Administrate/style.css b/macapp/Xcode/Gertrude/WebViews/Administrate/style.css index 107b289b..71643ab3 100644 --- a/macapp/Xcode/Gertrude/WebViews/Administrate/style.css +++ b/macapp/Xcode/Gertrude/WebViews/Administrate/style.css @@ -1 +1 @@ -*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[type=text],[type=email],[type=url],[type=password],[type=number],[type=date],[type=datetime-local],[type=month],[type=search],[type=tel],[type=time],[type=week],[multiple],textarea,select{-webkit-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow: 0 0 #0000}[type=text]:focus,[type=email]:focus,[type=url]:focus,[type=password]:focus,[type=number]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=month]:focus,[type=search]:focus,[type=tel]:focus,[type=time]:focus,[type=week]:focus,[multiple]:focus,textarea:focus,select:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple]{background-image:initial;background-position:initial;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow: 0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 2px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}[type=checkbox]:checked,[type=radio]:checked{border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e")}[type=radio]:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e")}[type=checkbox]:checked:hover,[type=checkbox]:checked:focus,[type=radio]:checked:hover,[type=radio]:checked:focus{border-color:transparent;background-color:currentColor}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}[type=checkbox]:indeterminate:hover,[type=checkbox]:indeterminate:focus{border-color:transparent;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::-webkit-backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.-left-full{left:-100%}.-right-6{right:-1.5rem}.bottom-0{bottom:0px}.left-0{left:0px}.left-4{left:1rem}.left-full{left:100%}.right-0{right:0px}.right-1{right:.25rem}.right-1\.5{right:.375rem}.right-2{right:.5rem}.top-0{top:0px}.top-1{top:.25rem}.top-2{top:.5rem}.top-4{top:1rem}.top-8{top:2rem}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.-m-2{margin:-.5rem}.mx-3{margin-left:.75rem;margin-right:.75rem}.mx-6{margin-left:1.5rem;margin-right:1.5rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-2\.5{margin-top:.625rem;margin-bottom:.625rem}.-mt-0{margin-top:-0px}.-mt-0\.5{margin-top:-.125rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-20{margin-bottom:5rem}.mb-3{margin-bottom:.75rem}.mb-3\.5{margin-bottom:.875rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.ml-16{margin-left:4rem}.ml-2{margin-left:.5rem}.ml-2\.5{margin-left:.625rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-6{margin-left:1.5rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mt-0{margin-top:0}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-20{height:5rem}.h-32{height:8rem}.h-4{height:1rem}.h-44{height:11rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-\[300px\]{height:300px}.h-full{height:100%}.h-screen{height:100vh}.min-h-screen{min-height:100vh}.w-1\/2{width:50%}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-32{width:8rem}.w-36{width:9rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-6{width:1.5rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-80{width:20rem}.w-9{width:2.25rem}.w-\[200px\]{width:200px}.w-\[400px\]{width:400px}.w-auto{width:auto}.w-fit{width:fit-content}.w-full{width:100%}.w-screen{width:100vw}.min-w-\[180px\]{min-width:180px}.\!max-w-none{max-width:none!important}.max-w-\[550px\]{max-width:550px}.max-w-fit{max-width:fit-content}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.flex-shrink-0,.shrink-0{flex-shrink:0}.flex-grow,.grow{flex-grow:1}.origin-top-right{transform-origin:top right}.-translate-y-1{--tw-translate-y: -.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-4{--tw-translate-x: 1rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-5{--tw-translate-x: 1.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-\[0\.5px\]{--tw-translate-x: .5px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0{--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0\.5{--tw-translate-y: .125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-1{--tw-translate-y: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-px{--tw-translate-y: 1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-180{--tw-rotate: -180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-0{--tw-rotate: 0deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-50{--tw-scale-x: .5;--tw-scale-y: .5;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-75{--tw-scale-x: .75;--tw-scale-y: .75;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.animate-\[loader-bounce_1\.5s_0\.1s_ease-out_infinite\]{animation:loader-bounce 1.5s .1s ease-out infinite}.animate-\[loader-bounce_1\.5s_0\.2s_ease-out_infinite\]{animation:loader-bounce 1.5s .2s ease-out infinite}@keyframes loader-bounce{0%{height:18px;opacity:1;width:18px;margin-left:0;margin-right:0;margin-top:0}15%{height:18px;opacity:1;width:18px;margin-left:0;margin-right:0;margin-top:0}30%{height:60px;opacity:.8;width:10px;margin-left:4px;margin-right:4px;margin-top:-4px}50%{height:18px;opacity:1;width:18px;margin-left:0;margin-right:0;margin-top:38px}65%{height:18px;opacity:1;width:18px;margin-left:0;margin-right:0;margin-top:38px}80%{height:60px;opacity:.8;width:10px;margin-left:4px;margin-right:4px;margin-top:0}to{height:18px;opacity:1;width:18px;margin-left:0;margin-right:0;margin-top:0}}.animate-\[loader-bounce_1\.5s_0\.3s_ease-out_infinite\]{animation:loader-bounce 1.5s .3s ease-out infinite}@keyframes ping{75%,to{transform:scale(2);opacity:0}}.animate-ping{animation:ping 1s cubic-bezier(0,0,.2,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;user-select:none}.resize-none{resize:none}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-x-2{column-gap:.5rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.25rem * var(--tw-space-x-reverse));margin-left:calc(.25rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-1\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.375rem * var(--tw-space-x-reverse));margin-left:calc(.375rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.75rem * var(--tw-space-x-reverse));margin-left:calc(.75rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1.25rem * var(--tw-space-x-reverse));margin-left:calc(1.25rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-6>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1.5rem * var(--tw-space-x-reverse));margin-left:calc(1.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.self-stretch{align-self:stretch}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-ellipsis,.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.rounded-2xl{border-radius:1rem}.rounded-\[30px\]{border-radius:30px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-b-2xl{border-bottom-right-radius:1rem;border-bottom-left-radius:1rem}.rounded-b-lg{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-b-xl{border-bottom-right-radius:.75rem;border-bottom-left-radius:.75rem}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-l-lg{border-top-left-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-l-xl{border-top-left-radius:.75rem;border-bottom-left-radius:.75rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-r-lg{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.rounded-r-none{border-top-right-radius:0;border-bottom-right-radius:0}.rounded-r-xl{border-top-right-radius:.75rem;border-bottom-right-radius:.75rem}.rounded-t-xl{border-top-left-radius:.75rem;border-top-right-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-\[0\.5px\]{border-width:.5px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l-0{border-left-width:0px}.border-r{border-right-width:1px}.border-r-0{border-right-width:0px}.border-t{border-top-width:1px}.border-none{border-style:none}.\!border-red-500{--tw-border-opacity: 1 !important;border-color:rgb(239 68 68 / var(--tw-border-opacity))!important}.\!border-violet-200{--tw-border-opacity: 1 !important;border-color:rgb(221 214 254 / var(--tw-border-opacity))!important}.border-blue-200{--tw-border-opacity: 1;border-color:rgb(191 219 254 / var(--tw-border-opacity))}.border-fuchsia-500{--tw-border-opacity: 1;border-color:rgb(217 70 239 / var(--tw-border-opacity))}.border-green-200{--tw-border-opacity: 1;border-color:rgb(187 247 208 / var(--tw-border-opacity))}.border-indigo-500{--tw-border-opacity: 1;border-color:rgb(99 102 241 / var(--tw-border-opacity))}.border-red-100{--tw-border-opacity: 1;border-color:rgb(254 226 226 / var(--tw-border-opacity))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity))}.border-red-200\/70{border-color:#fecacab3}.border-red-500\/20{border-color:#ef444433}.border-slate-200{--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity))}.border-slate-300{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity))}.border-slate-900{--tw-border-opacity: 1;border-color:rgb(15 23 42 / var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-violet-100{--tw-border-opacity: 1;border-color:rgb(237 233 254 / var(--tw-border-opacity))}.border-violet-700{--tw-border-opacity: 1;border-color:rgb(109 40 217 / var(--tw-border-opacity))}.border-white{--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity))}.border-white\/30{border-color:#ffffff4d}.border-yellow-200{--tw-border-opacity: 1;border-color:rgb(254 240 138 / var(--tw-border-opacity))}.border-yellow-500\/20{border-color:#eab30833}.\!bg-violet-100{--tw-bg-opacity: 1 !important;background-color:rgb(237 233 254 / var(--tw-bg-opacity))!important}.bg-black\/5{background-color:#0000000d}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity))}.bg-fuchsia-500{--tw-bg-opacity: 1;background-color:rgb(217 70 239 / var(--tw-bg-opacity))}.bg-gray-500\/90{background-color:#6b7280e6}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity))}.bg-indigo-300\/40{background-color:#a5b4fc66}.bg-indigo-500{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity))}.bg-purple-500{--tw-bg-opacity: 1;background-color:rgb(168 85 247 / var(--tw-bg-opacity))}.bg-red-400{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity))}.bg-red-50\/20{background-color:#fef2f233}.bg-red-50\/30{background-color:#fef2f24d}.bg-red-50\/50{background-color:#fef2f280}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.bg-red-500\/20{background-color:#ef444433}.bg-slate-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.bg-slate-200{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity))}.bg-slate-50{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity))}.bg-slate-50\/30{background-color:#f8fafc4d}.bg-slate-50\/50{background-color:#f8fafc80}.bg-slate-500\/70{background-color:#64748bb3}.bg-violet-100{--tw-bg-opacity: 1;background-color:rgb(237 233 254 / var(--tw-bg-opacity))}.bg-violet-500{--tw-bg-opacity: 1;background-color:rgb(139 92 246 / var(--tw-bg-opacity))}.bg-violet-500\/90{background-color:#8b5cf6e6}.bg-violet-700{--tw-bg-opacity: 1;background-color:rgb(109 40 217 / var(--tw-bg-opacity))}.bg-violet-800{--tw-bg-opacity: 1;background-color:rgb(91 33 182 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-white\/20{background-color:#fff3}.bg-white\/30{background-color:#ffffff4d}.bg-white\/80{background-color:#fffc}.bg-white\/90{background-color:#ffffffe6}.bg-yellow-400{--tw-bg-opacity: 1;background-color:rgb(250 204 21 / var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity: 1;background-color:rgb(254 252 232 / var(--tw-bg-opacity))}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity))}.bg-opacity-0{--tw-bg-opacity: 0}.bg-opacity-30{--tw-bg-opacity: .3}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-tr{background-image:linear-gradient(to top right,var(--tw-gradient-stops))}.from-\[\#ffffff00\]{--tw-gradient-from: #ffffff00 var(--tw-gradient-from-position);--tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-indigo-400{--tw-gradient-from: #818cf8 var(--tw-gradient-from-position);--tw-gradient-to: rgb(129 140 248 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-indigo-500{--tw-gradient-from: #6366f1 var(--tw-gradient-from-position);--tw-gradient-to: rgb(99 102 241 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-indigo-600{--tw-gradient-from: #4f46e5 var(--tw-gradient-from-position);--tw-gradient-to: rgb(79 70 229 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-fuchsia-400{--tw-gradient-to: #e879f9 var(--tw-gradient-to-position)}.to-fuchsia-500{--tw-gradient-to: #d946ef var(--tw-gradient-to-position)}.to-fuchsia-600{--tw-gradient-to: #c026d3 var(--tw-gradient-to-position)}.bg-clip-text{background-clip:text}.p-10{padding:2.5rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-3\.5{padding:.875rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-12{padding-left:3rem;padding-right:3rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-7{padding-left:1.75rem;padding-right:1.75rem}.px-8{padding-left:2rem;padding-right:2rem}.px-\[12px\]{padding-left:12px;padding-right:12px}.px-\[14px\]{padding-left:14px;padding-right:14px}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-\[2\.5px\]{padding-top:2.5px;padding-bottom:2.5px}.py-\[2px\]{padding-top:2px;padding-bottom:2px}.pb-1{padding-bottom:.25rem}.pb-8{padding-bottom:2rem}.pb-px{padding-bottom:1px}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pl-8{padding-left:2rem}.pr-2{padding-right:.5rem}.pr-4{padding-right:1rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.font-inter{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.font-lato{font-family:lato,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-6xl{font-size:3.75rem;line-height:1}.text-\[13px\]{font-size:13px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-5{line-height:1.25rem}.leading-tight{line-height:1.25}.tracking-wider{letter-spacing:.05em}.\!text-violet-700{--tw-text-opacity: 1 !important;color:rgb(109 40 217 / var(--tw-text-opacity))!important}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.text-black\/10{color:#0000001a}.text-black\/20{color:#0003}.text-black\/30{color:#0000004d}.text-black\/50{color:#00000080}.text-black\/70{color:#000000b3}.text-black\/80{color:#000c}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}.text-fuchsia-800{--tw-text-opacity: 1;color:rgb(134 25 143 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-gray-600\/80{color:#4b5563cc}.text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity))}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity))}.text-red-700\/70{color:#b91c1cb3}.text-slate-100{--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}.text-slate-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.text-slate-400\/80{color:#94a3b8cc}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}.text-slate-600{--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity))}.text-slate-700{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity))}.text-slate-800{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity))}.text-slate-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity))}.text-transparent{color:transparent}.text-violet-500{--tw-text-opacity: 1;color:rgb(139 92 246 / var(--tw-text-opacity))}.text-violet-500\/80{color:#8b5cf6cc}.text-violet-600{--tw-text-opacity: 1;color:rgb(124 58 237 / var(--tw-text-opacity))}.text-violet-700{--tw-text-opacity: 1;color:rgb(109 40 217 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-yellow-200{--tw-text-opacity: 1;color:rgb(254 240 138 / var(--tw-text-opacity))}.text-yellow-600{--tw-text-opacity: 1;color:rgb(202 138 4 / var(--tw-text-opacity))}.text-opacity-90{--tw-text-opacity: .9}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-80{opacity:.8}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-black\/5{--tw-shadow-color: rgb(0 0 0 / .05);--tw-shadow: var(--tw-shadow-colored)}.shadow-slate-300\/30{--tw-shadow-color: rgb(203 213 225 / .3);--tw-shadow: var(--tw-shadow-colored)}.shadow-slate-300\/50{--tw-shadow-color: rgb(203 213 225 / .5);--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-0{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-slate-200{--tw-ring-opacity: 1;--tw-ring-color: rgb(226 232 240 / var(--tw-ring-opacity))}.ring-transparent{--tw-ring-color: transparent}.ring-violet-500{--tw-ring-opacity: 1;--tw-ring-color: rgb(139 92 246 / var(--tw-ring-opacity))}.ring-offset-0{--tw-ring-offset-width: 0px}.ring-offset-violet-500{--tw-ring-offset-color: #8b5cf6}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-md{--tw-backdrop-blur: blur(12px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-none{--tw-backdrop-blur: blur(0);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur: blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[backdrop-filter\,background-color\]{transition-property:background-color,-webkit-backdrop-filter;transition-property:backdrop-filter,background-color;transition-property:backdrop-filter,background-color,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[background-color\,box-shadow\,transform\]{transition-property:background-color,box-shadow,transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[background-color\,box-shadow\]{transition-property:background-color,box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[background-color\,opacity\,transform\]{transition-property:background-color,opacity,transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[background-color\,transform\,box-shadow\]{transition-property:background-color,transform,box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[background-color\]{transition-property:background-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[border-color\,background-color\]{transition-property:border-color,background-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[border-color\,ring-color\]{transition-property:border-color,ring-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[box-shadow\,transform\,background-color\]{transition-property:box-shadow,transform,background-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[color\,transform\,opacity\]{transition-property:color,transform,opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[height\]{transition-property:height;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[left\,opacity\]{transition-property:left,opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[opacity\,display\,background-color\]{transition-property:opacity,display,background-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[opacity\,transform\]{transition-property:opacity,transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[outline\,background-color\]{transition-property:outline,background-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[padding-bottom\,border-color\]{transition-property:padding-bottom,border-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[transform\,background-color\,box-shadow\]{transition-property:transform,background-color,box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[transform\,border-color\,border\,background-color\]{transition-property:transform,border-color,border,background-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[transform\,opacity\]{transition-property:transform,opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-75{transition-duration:75ms}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.\[-webkit-background-clip\:text\;\],.\[-webkit-background-clip\:text\]{-webkit-background-clip:text}*{touch-callout:none;user-select:none;-webkit-touch-callout:none;-webkit-user-select:none}input,textarea{touch-callout:default;user-select:auto;-webkit-touch-callout:default;-webkit-user-select:auto}.dark *::-webkit-scrollbar,body.dark::-webkit-scrollbar{width:12px}.dark *::-webkit-scrollbar-track,body.dark::-webkit-scrollbar-track{background-color:#0f172a}.dark *::-webkit-scrollbar-thumb,body.dark::-webkit-scrollbar-thumb{background-color:#1e293b;border-radius:6px}.dark *::-webkit-scrollbar-thumb:hover,body.dark::-webkit-scrollbar-thumb{background-color:#334155}.placeholder\:text-slate-400::placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.placeholder\:text-slate-400\/90::placeholder{color:#94a3b8e6}.placeholder\:antialiased::placeholder{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.before\:\[content\:\'-_\'\]:before{content:"- "}.after\:\[content\:\'_-\'\]:after{content:" -"}.even\:bg-slate-50:nth-child(even){--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity))}.even\:text-slate-50:nth-child(even){--tw-text-opacity: 1;color:rgb(248 250 252 / var(--tw-text-opacity))}.hover\:-translate-y-0:hover{--tw-translate-y: -0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:-translate-y-0\.5:hover{--tw-translate-y: -.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:-translate-y-\[1px\]:hover{--tw-translate-y: -1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-105:hover{--tw-scale-x: 1.05;--tw-scale-y: 1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-slate-400:hover{--tw-border-opacity: 1;border-color:rgb(148 163 184 / var(--tw-border-opacity))}.hover\:border-slate-600:hover{--tw-border-opacity: 1;border-color:rgb(71 85 105 / var(--tw-border-opacity))}.hover\:border-violet-200:hover{--tw-border-opacity: 1;border-color:rgb(221 214 254 / var(--tw-border-opacity))}.hover\:border-violet-50:hover{--tw-border-opacity: 1;border-color:rgb(245 243 255 / var(--tw-border-opacity))}.hover\:border-violet-800:hover{--tw-border-opacity: 1;border-color:rgb(91 33 182 / var(--tw-border-opacity))}.hover\:\!bg-violet-200:hover{--tw-bg-opacity: 1 !important;background-color:rgb(221 214 254 / var(--tw-bg-opacity))!important}.hover\:bg-red-100:hover{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity))}.hover\:bg-slate-100:hover{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.hover\:bg-slate-200:hover{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity))}.hover\:bg-slate-300:hover{--tw-bg-opacity: 1;background-color:rgb(203 213 225 / var(--tw-bg-opacity))}.hover\:bg-slate-50:hover{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity))}.hover\:bg-slate-600\/70:hover{background-color:#475569b3}.hover\:bg-violet-200:hover{--tw-bg-opacity: 1;background-color:rgb(221 214 254 / var(--tw-bg-opacity))}.hover\:bg-violet-400:hover{--tw-bg-opacity: 1;background-color:rgb(167 139 250 / var(--tw-bg-opacity))}.hover\:bg-violet-50:hover{--tw-bg-opacity: 1;background-color:rgb(245 243 255 / var(--tw-bg-opacity))}.hover\:bg-violet-600\/90:hover{background-color:#7c3aede6}.hover\:bg-violet-800:hover{--tw-bg-opacity: 1;background-color:rgb(91 33 182 / var(--tw-bg-opacity))}.hover\:bg-violet-900:hover{--tw-bg-opacity: 1;background-color:rgb(76 29 149 / var(--tw-bg-opacity))}.hover\:bg-white\/100:hover{background-color:#fff}.hover\:bg-white\/20:hover{background-color:#fff3}.hover\:bg-white\/30:hover{background-color:#ffffff4d}.hover\:bg-white\/80:hover{background-color:#fffc}.hover\:from-indigo-600:hover{--tw-gradient-from: #4f46e5 var(--tw-gradient-from-position);--tw-gradient-to: rgb(79 70 229 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.hover\:to-fuchsia-600:hover{--tw-gradient-to: #c026d3 var(--tw-gradient-to-position)}.hover\:pb-0:hover{padding-bottom:0}.hover\:pb-0\.5:hover{padding-bottom:.125rem}.hover\:text-black:hover{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.hover\:text-black\/30:hover{color:#0000004d}.hover\:text-black\/40:hover{color:#0006}.hover\:text-red-700:hover{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity))}.hover\:text-slate-600:hover{--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity))}.hover\:shadow-lg:hover{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:z-10:focus{z-index:10}.focus\:border-indigo-200:focus{--tw-border-opacity: 1;border-color:rgb(199 210 254 / var(--tw-border-opacity))}.focus\:border-indigo-500:focus{--tw-border-opacity: 1;border-color:rgb(99 102 241 / var(--tw-border-opacity))}.focus\:border-red-500:focus{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity))}.focus\:bg-slate-50:focus{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity))}.focus\:shadow-md:focus{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-indigo-400\/50:focus{--tw-ring-color: rgb(129 140 248 / .5)}.focus\:ring-indigo-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity))}.focus\:ring-red-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity))}.focus\:ring-slate-200:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(226 232 240 / var(--tw-ring-opacity))}.focus\:ring-violet-300:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(196 181 253 / var(--tw-ring-opacity))}.focus\:ring-violet-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(139 92 246 / var(--tw-ring-opacity))}.focus\:ring-violet-700:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(109 40 217 / var(--tw-ring-opacity))}.focus\:ring-white:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity))}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus\:ring-offset-slate-50:focus{--tw-ring-offset-color: #f8fafc}.active\:translate-y-0:active{--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:scale-95:active{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:shadow:active{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.group:nth-child(odd) .group-odd\:bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.group:nth-child(odd) .group-odd\:to-white{--tw-gradient-to: #fff var(--tw-gradient-to-position)}.group:nth-child(even) .group-even\:bg-slate-50{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity))}.group:nth-child(even) .group-even\:to-slate-50{--tw-gradient-to: #f8fafc var(--tw-gradient-to-position)}.group:hover .group-hover\:bg-slate-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.group:hover .group-hover\:to-slate-100{--tw-gradient-to: #f1f5f9 var(--tw-gradient-to-position)}:is(.dark .dark\:border){border-width:1px}:is(.dark .dark\:\!border-violet-600){--tw-border-opacity: 1 !important;border-color:rgb(124 58 237 / var(--tw-border-opacity))!important}:is(.dark .dark\:border-blue-500\/50){border-color:#3b82f680}:is(.dark .dark\:border-fuchsia-400){--tw-border-opacity: 1;border-color:rgb(232 121 249 / var(--tw-border-opacity))}:is(.dark .dark\:border-green-500\/50){border-color:#22c55e80}:is(.dark .dark\:border-green-500\/70){border-color:#22c55eb3}:is(.dark .dark\:border-indigo-400){--tw-border-opacity: 1;border-color:rgb(129 140 248 / var(--tw-border-opacity))}:is(.dark .dark\:border-red-500){--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity))}:is(.dark .dark\:border-red-500\/20){border-color:#ef444433}:is(.dark .dark\:border-red-500\/40){border-color:#ef444466}:is(.dark .dark\:border-red-500\/50){border-color:#ef444480}:is(.dark .dark\:border-red-500\/70){border-color:#ef4444b3}:is(.dark .dark\:border-red-600\/50){border-color:#dc262680}:is(.dark .dark\:border-red-700\/50){border-color:#b91c1c80}:is(.dark .dark\:border-slate-500){--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity))}:is(.dark .dark\:border-slate-600){--tw-border-opacity: 1;border-color:rgb(71 85 105 / var(--tw-border-opacity))}:is(.dark .dark\:border-slate-700){--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity))}:is(.dark .dark\:border-slate-700\/50){border-color:#33415580}:is(.dark .dark\:border-slate-700\/70){border-color:#334155b3}:is(.dark .dark\:border-slate-800){--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity))}:is(.dark .dark\:border-transparent){border-color:transparent}:is(.dark .dark\:border-violet-700){--tw-border-opacity: 1;border-color:rgb(109 40 217 / var(--tw-border-opacity))}:is(.dark .dark\:border-white\/20){border-color:#fff3}:is(.dark .dark\:border-yellow-500\/50){border-color:#eab30880}:is(.dark .dark\:border-yellow-500\/70){border-color:#eab308b3}:is(.dark .dark\:\!bg-violet-700\/60){background-color:#6d28d999!important}:is(.dark .dark\:bg-black\/20){background-color:#0003}:is(.dark .dark\:bg-black\/50){background-color:#00000080}:is(.dark .dark\:bg-blue-500\/10){background-color:#3b82f61a}:is(.dark .dark\:bg-fuchsia-500\/20){background-color:#d946ef33}:is(.dark .dark\:bg-green-500\/10){background-color:#22c55e1a}:is(.dark .dark\:bg-green-500\/30){background-color:#22c55e4d}:is(.dark .dark\:bg-indigo-500\/20){background-color:#6366f133}:is(.dark .dark\:bg-indigo-600\/50){background-color:#4f46e580}:is(.dark .dark\:bg-purple-800){--tw-bg-opacity: 1;background-color:rgb(107 33 168 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-500\/10){background-color:#ef44441a}:is(.dark .dark\:bg-red-500\/20){background-color:#ef444433}:is(.dark .dark\:bg-red-500\/30){background-color:#ef44444d}:is(.dark .dark\:bg-red-500\/5){background-color:#ef44440d}:is(.dark .dark\:bg-red-600\/5){background-color:#dc26260d}:is(.dark .dark\:bg-slate-200){--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-slate-700){--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-slate-700\/20){background-color:#33415533}:is(.dark .dark\:bg-slate-700\/50){background-color:#33415580}:is(.dark .dark\:bg-slate-800){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-slate-800\/30){background-color:#1e293b4d}:is(.dark .dark\:bg-slate-800\/50){background-color:#1e293b80}:is(.dark .dark\:bg-slate-800\/60){background-color:#1e293b99}:is(.dark .dark\:bg-slate-800\/80){background-color:#1e293bcc}:is(.dark .dark\:bg-slate-900){--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-slate-900\/50){background-color:#0f172a80}:is(.dark .dark\:bg-violet-600){--tw-bg-opacity: 1;background-color:rgb(124 58 237 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-violet-700){--tw-bg-opacity: 1;background-color:rgb(109 40 217 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-violet-700\/90){background-color:#6d28d9e6}:is(.dark .dark\:bg-white\/20){background-color:#fff3}:is(.dark .dark\:bg-white\/5){background-color:#ffffff0d}:is(.dark .dark\:bg-white\/80){background-color:#fffc}:is(.dark .dark\:bg-yellow-500\/10){background-color:#eab3081a}:is(.dark .dark\:bg-yellow-500\/30){background-color:#eab3084d}:is(.dark .dark\:bg-opacity-0){--tw-bg-opacity: 0}:is(.dark .dark\:bg-opacity-90){--tw-bg-opacity: .9}:is(.dark .dark\:from-\[\#0f172a00\]){--tw-gradient-from: #0f172a00 var(--tw-gradient-from-position);--tw-gradient-to: rgb(15 23 42 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}:is(.dark .dark\:\!text-violet-100){--tw-text-opacity: 1 !important;color:rgb(237 233 254 / var(--tw-text-opacity))!important}:is(.dark .dark\:text-blue-300){--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity))}:is(.dark .dark\:text-fuchsia-200){--tw-text-opacity: 1;color:rgb(245 208 254 / var(--tw-text-opacity))}:is(.dark .dark\:text-green-300){--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity))}:is(.dark .dark\:text-indigo-200){--tw-text-opacity: 1;color:rgb(199 210 254 / var(--tw-text-opacity))}:is(.dark .dark\:text-indigo-300){--tw-text-opacity: 1;color:rgb(165 180 252 / var(--tw-text-opacity))}:is(.dark .dark\:text-red-100){--tw-text-opacity: 1;color:rgb(254 226 226 / var(--tw-text-opacity))}:is(.dark .dark\:text-red-200){--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}:is(.dark .dark\:text-red-300){--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity))}:is(.dark .dark\:text-red-400){--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity))}:is(.dark .dark\:text-red-400\/50){color:#f8717180}:is(.dark .dark\:text-slate-100){--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}:is(.dark .dark\:text-slate-200){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity))}:is(.dark .dark\:text-slate-300){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}:is(.dark .dark\:text-slate-400){--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}:is(.dark .dark\:text-slate-500){--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}:is(.dark .dark\:text-slate-600){--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity))}:is(.dark .dark\:text-slate-700){--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity))}:is(.dark .dark\:text-slate-900){--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity))}:is(.dark .dark\:text-violet-400){--tw-text-opacity: 1;color:rgb(167 139 250 / var(--tw-text-opacity))}:is(.dark .dark\:text-white){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}:is(.dark .dark\:text-white\/10){color:#ffffff1a}:is(.dark .dark\:text-white\/20){color:#fff3}:is(.dark .dark\:text-white\/30){color:#ffffff4d}:is(.dark .dark\:text-white\/50){color:#ffffff80}:is(.dark .dark\:text-white\/60){color:#fff9}:is(.dark .dark\:text-white\/70){color:#ffffffb3}:is(.dark .dark\:text-white\/80){color:#fffc}:is(.dark .dark\:text-yellow-100){--tw-text-opacity: 1;color:rgb(254 249 195 / var(--tw-text-opacity))}:is(.dark .dark\:text-yellow-200){--tw-text-opacity: 1;color:rgb(254 240 138 / var(--tw-text-opacity))}:is(.dark .dark\:text-yellow-300){--tw-text-opacity: 1;color:rgb(253 224 71 / var(--tw-text-opacity))}:is(.dark .dark\:opacity-90){opacity:.9}:is(.dark .dark\:shadow-lg){--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}:is(.dark .dark\:shadow-black\/20){--tw-shadow-color: rgb(0 0 0 / .2);--tw-shadow: var(--tw-shadow-colored)}:is(.dark .dark\:shadow-black\/30){--tw-shadow-color: rgb(0 0 0 / .3);--tw-shadow: var(--tw-shadow-colored)}:is(.dark .dark\:shadow-black\/50){--tw-shadow-color: rgb(0 0 0 / .5);--tw-shadow: var(--tw-shadow-colored)}:is(.dark .dark\:ring-offset-slate-900){--tw-ring-offset-color: #0f172a}:is(.dark .dark\:backdrop-blur-sm){--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}:is(.dark .dark\:\[filter\:brightness\(600\%\)\]){filter:brightness(600%)}:is(.dark .dark\:placeholder\:text-slate-500)::placeholder{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}:is(.dark .dark\:even\:bg-\[\#141A2F\]:nth-child(even)){--tw-bg-opacity: 1;background-color:rgb(20 26 47 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:border-slate-100:hover){--tw-border-opacity: 1;border-color:rgb(241 245 249 / var(--tw-border-opacity))}:is(.dark .dark\:hover\:border-slate-500:hover){--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity))}:is(.dark .dark\:hover\:border-violet-700:hover){--tw-border-opacity: 1;border-color:rgb(109 40 217 / var(--tw-border-opacity))}:is(.dark .dark\:hover\:\!bg-violet-700:hover){--tw-bg-opacity: 1 !important;background-color:rgb(109 40 217 / var(--tw-bg-opacity))!important}:is(.dark .dark\:hover\:bg-red-500\/20:hover){background-color:#ef444433}:is(.dark .dark\:hover\:bg-slate-600:hover){--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-slate-700\/80:hover){background-color:#334155cc}:is(.dark .dark\:hover\:bg-slate-800:hover){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-slate-800\/50:hover){background-color:#1e293b80}:is(.dark .dark\:hover\:bg-violet-700:hover){--tw-bg-opacity: 1;background-color:rgb(109 40 217 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-violet-800\/90:hover){background-color:#5b21b6e6}:is(.dark .dark\:hover\:bg-white:hover){--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-white\/10:hover){background-color:#ffffff1a}:is(.dark .dark\:hover\:bg-white\/30:hover){background-color:#ffffff4d}:is(.dark .hover\:dark\:bg-slate-900\/80):hover{background-color:#0f172acc}:is(.dark .dark\:hover\:text-red-200:hover){--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-slate-400:hover){--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-white:hover){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}:is(.dark .hover\:dark\:text-white\/30):hover{color:#ffffff4d}:is(.dark .hover\:dark\:text-white\/40):hover{color:#fff6}:is(.dark .dark\:focus\:bg-slate-800:focus){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}:is(.dark .dark\:focus\:ring-indigo-500\/70:focus){--tw-ring-color: rgb(99 102 241 / .7)}:is(.dark .group:nth-child(odd) .dark\:group-odd\:bg-slate-900){--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity))}:is(.dark .group:nth-child(odd) .dark\:group-odd\:to-slate-900){--tw-gradient-to: #0f172a var(--tw-gradient-to-position)}:is(.dark .group:nth-child(even) .dark\:group-even\:bg-\[\#141A2E\]){--tw-bg-opacity: 1;background-color:rgb(20 26 46 / var(--tw-bg-opacity))}:is(.dark .group:nth-child(even) .dark\:group-even\:to-\[\#141A2E\]){--tw-gradient-to: #141A2E var(--tw-gradient-to-position)}:is(.dark .group:hover .dark\:group-hover\:bg-slate-800){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}:is(.dark .group:hover .dark\:group-hover\:to-slate-800){--tw-gradient-to: #1e293b var(--tw-gradient-to-position)}@media (min-width: 500px){.xs\:flex{display:flex}.xs\:rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}}@media (min-width: 640px){.sm\:py-2{padding-top:.5rem;padding-bottom:.5rem}.sm\:py-2\.5{padding-top:.625rem;padding-bottom:.625rem}} +*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[type=text],[type=email],[type=url],[type=password],[type=number],[type=date],[type=datetime-local],[type=month],[type=search],[type=tel],[type=time],[type=week],[multiple],textarea,select{-webkit-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow: 0 0 #0000}[type=text]:focus,[type=email]:focus,[type=url]:focus,[type=password]:focus,[type=number]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=month]:focus,[type=search]:focus,[type=tel]:focus,[type=time]:focus,[type=week]:focus,[multiple]:focus,textarea:focus,select:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field{padding-top:0;padding-bottom:0}select{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple]{background-image:initial;background-position:initial;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow: 0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 2px;--tw-ring-offset-color: #fff;--tw-ring-color: #2563eb;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}[type=checkbox]:checked,[type=radio]:checked{border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e")}[type=radio]:checked{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e")}[type=checkbox]:checked:hover,[type=checkbox]:checked:focus,[type=radio]:checked:hover,[type=radio]:checked:focus{border-color:transparent;background-color:currentColor}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:center;background-repeat:no-repeat}[type=checkbox]:indeterminate:hover,[type=checkbox]:indeterminate:focus{border-color:transparent;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::-webkit-backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.-bottom-72{bottom:-18rem}.-bottom-80{bottom:-20rem}.-bottom-96{bottom:-24rem}.-left-96{left:-24rem}.-left-full{left:-100%}.-right-6{right:-1.5rem}.-right-80{right:-20rem}.-top-12{top:-3rem}.-top-6{top:-1.5rem}.-top-\[200vh\]{top:-200vh}.bottom-0{bottom:0px}.left-0{left:0px}.left-4{left:1rem}.left-\[-100\%\]{left:-100%}.left-\[50vw\]{left:50vw}.left-full{left:100%}.right-0{right:0px}.right-1{right:.25rem}.right-1\.5{right:.375rem}.right-2{right:.5rem}.top-0{top:0px}.top-1{top:.25rem}.top-2{top:.5rem}.top-4{top:1rem}.top-8{top:2rem}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.-m-2{margin:-.5rem}.m-1{margin:.25rem}.m-4{margin:1rem}.mx-3{margin-left:.75rem;margin-right:.75rem}.mx-6{margin-left:1.5rem;margin-right:1.5rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-2\.5{margin-top:.625rem;margin-bottom:.625rem}.my-4{margin-top:1rem;margin-bottom:1rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.my-8{margin-top:2rem;margin-bottom:2rem}.-mt-0{margin-top:-0px}.-mt-0\.5{margin-top:-.125rem}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-2{margin-bottom:.5rem}.mb-20{margin-bottom:5rem}.mb-3{margin-bottom:.75rem}.mb-3\.5{margin-bottom:.875rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-16{margin-left:4rem}.ml-2{margin-left:.5rem}.ml-2\.5{margin-left:.625rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-6{margin-left:1.5rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mt-0{margin-top:0}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-152{height:38rem}.h-2{height:.5rem}.h-20{height:5rem}.h-32{height:8rem}.h-4{height:1rem}.h-44{height:11rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-\[200px\]{height:200px}.h-\[200vh\]{height:200vh}.h-\[300px\]{height:300px}.h-\[3px\]{height:3px}.h-full{height:100%}.h-screen{height:100vh}.min-h-screen{min-height:100vh}.w-1\/2{width:50%}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-152{width:38rem}.w-16{width:4rem}.w-2{width:.5rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-32{width:8rem}.w-36{width:9rem}.w-4{width:1rem}.w-44{width:11rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-80{width:20rem}.w-9{width:2.25rem}.w-\[200px\]{width:200px}.w-\[400px\]{width:400px}.w-auto{width:auto}.w-fit{width:fit-content}.w-full{width:100%}.w-screen{width:100vw}.min-w-\[180px\]{min-width:180px}.\!max-w-none{max-width:none!important}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-\[550px\]{max-width:550px}.max-w-fit{max-width:fit-content}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-shrink-0,.shrink-0{flex-shrink:0}.flex-grow,.grow{flex-grow:1}.origin-top-right{transform-origin:top right}.-translate-y-1{--tw-translate-y: -.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-16{--tw-translate-y: -4rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-20{--tw-translate-y: -5rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-8{--tw-translate-y: -2rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-4{--tw-translate-x: 1rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-5{--tw-translate-x: 1.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-\[0\.5px\]{--tw-translate-x: .5px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0{--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0\.5{--tw-translate-y: .125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-1{--tw-translate-y: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-12{--tw-translate-y: 3rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-4{--tw-translate-y: 1rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-6{--tw-translate-y: 1.5rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-8{--tw-translate-y: 2rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-px{--tw-translate-y: 1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-180{--tw-rotate: -180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-0{--tw-rotate: 0deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-50{--tw-scale-x: .5;--tw-scale-y: .5;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-75{--tw-scale-x: .75;--tw-scale-y: .75;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-\[350\%\]{--tw-scale-x: 350%;--tw-scale-y: 350%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.animate-\[loader-bounce_1\.5s_0\.1s_ease-out_infinite\]{animation:loader-bounce 1.5s .1s ease-out infinite}.animate-\[loader-bounce_1\.5s_0\.2s_ease-out_infinite\]{animation:loader-bounce 1.5s .2s ease-out infinite}@keyframes loader-bounce{0%{height:18px;opacity:1;width:18px;margin-left:0;margin-right:0;margin-top:0}15%{height:18px;opacity:1;width:18px;margin-left:0;margin-right:0;margin-top:0}30%{height:60px;opacity:.8;width:10px;margin-left:4px;margin-right:4px;margin-top:-4px}50%{height:18px;opacity:1;width:18px;margin-left:0;margin-right:0;margin-top:38px}65%{height:18px;opacity:1;width:18px;margin-left:0;margin-right:0;margin-top:38px}80%{height:60px;opacity:.8;width:10px;margin-left:4px;margin-right:4px;margin-top:0}to{height:18px;opacity:1;width:18px;margin-left:0;margin-right:0;margin-top:0}}.animate-\[loader-bounce_1\.5s_0\.3s_ease-out_infinite\]{animation:loader-bounce 1.5s .3s ease-out infinite}@keyframes bounce{0%,to{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,.2,1)}}.animate-bounce{animation:bounce 1s infinite}@keyframes ping{75%,to{transform:scale(2);opacity:0}}.animate-ping{animation:ping 1s cubic-bezier(0,0,.2,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.cursor-zoom-in{cursor:zoom-in}.cursor-zoom-out{cursor:zoom-out}.select-none{-webkit-user-select:none;user-select:none}.resize-none{resize:none}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-2{column-gap:.5rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.25rem * var(--tw-space-x-reverse));margin-left:calc(.25rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-1\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.375rem * var(--tw-space-x-reverse));margin-left:calc(.375rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.75rem * var(--tw-space-x-reverse));margin-left:calc(.75rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1.25rem * var(--tw-space-x-reverse));margin-left:calc(1.25rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-6>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1.5rem * var(--tw-space-x-reverse));margin-left:calc(1.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.self-end{align-self:flex-end}.self-stretch{align-self:stretch}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-ellipsis,.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.break-words{overflow-wrap:break-word}.\!rounded-lg{border-radius:.5rem!important}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-\[30px\]{border-radius:30px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-b-2xl{border-bottom-right-radius:1rem;border-bottom-left-radius:1rem}.rounded-b-3xl{border-bottom-right-radius:1.5rem;border-bottom-left-radius:1.5rem}.rounded-b-lg{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-b-xl{border-bottom-right-radius:.75rem;border-bottom-left-radius:.75rem}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-l-xl{border-top-left-radius:.75rem;border-bottom-left-radius:.75rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-r-none{border-top-right-radius:0;border-bottom-right-radius:0}.rounded-r-xl{border-top-right-radius:.75rem;border-bottom-right-radius:.75rem}.rounded-t-xl{border-top-left-radius:.75rem;border-top-right-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-\[0\.5px\]{border-width:.5px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l-0{border-left-width:0px}.border-r{border-right-width:1px}.border-r-0{border-right-width:0px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-none{border-style:none}.\!border-red-500{--tw-border-opacity: 1 !important;border-color:rgb(239 68 68 / var(--tw-border-opacity))!important}.\!border-violet-200{--tw-border-opacity: 1 !important;border-color:rgb(221 214 254 / var(--tw-border-opacity))!important}.border-\[\#eceff7\]{--tw-border-opacity: 1;border-color:rgb(236 239 247 / var(--tw-border-opacity))}.border-blue-200{--tw-border-opacity: 1;border-color:rgb(191 219 254 / var(--tw-border-opacity))}.border-fuchsia-500{--tw-border-opacity: 1;border-color:rgb(217 70 239 / var(--tw-border-opacity))}.border-green-200{--tw-border-opacity: 1;border-color:rgb(187 247 208 / var(--tw-border-opacity))}.border-indigo-500{--tw-border-opacity: 1;border-color:rgb(99 102 241 / var(--tw-border-opacity))}.border-red-100{--tw-border-opacity: 1;border-color:rgb(254 226 226 / var(--tw-border-opacity))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity))}.border-red-200\/70{border-color:#fecacab3}.border-red-500\/20{border-color:#ef444433}.border-slate-200{--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity))}.border-slate-200\/50{border-color:#e2e8f080}.border-slate-200\/60{border-color:#e2e8f099}.border-slate-300{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity))}.border-slate-900{--tw-border-opacity: 1;border-color:rgb(15 23 42 / var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-violet-100{--tw-border-opacity: 1;border-color:rgb(237 233 254 / var(--tw-border-opacity))}.border-violet-300{--tw-border-opacity: 1;border-color:rgb(196 181 253 / var(--tw-border-opacity))}.border-violet-500{--tw-border-opacity: 1;border-color:rgb(139 92 246 / var(--tw-border-opacity))}.border-violet-700{--tw-border-opacity: 1;border-color:rgb(109 40 217 / var(--tw-border-opacity))}.border-white{--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity))}.border-white\/30{border-color:#ffffff4d}.border-yellow-200{--tw-border-opacity: 1;border-color:rgb(254 240 138 / var(--tw-border-opacity))}.border-yellow-500\/20{border-color:#eab30833}.\!bg-violet-100{--tw-bg-opacity: 1 !important;background-color:rgb(237 233 254 / var(--tw-bg-opacity))!important}.bg-\[\#eceff7\]{--tw-bg-opacity: 1;background-color:rgb(236 239 247 / var(--tw-bg-opacity))}.bg-black\/5{background-color:#0000000d}.bg-black\/50{background-color:#00000080}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity))}.bg-fuchsia-50{--tw-bg-opacity: 1;background-color:rgb(253 244 255 / var(--tw-bg-opacity))}.bg-fuchsia-500{--tw-bg-opacity: 1;background-color:rgb(217 70 239 / var(--tw-bg-opacity))}.bg-gray-500\/90{background-color:#6b7280e6}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity))}.bg-indigo-300\/40{background-color:#a5b4fc66}.bg-indigo-500{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity))}.bg-purple-500{--tw-bg-opacity: 1;background-color:rgb(168 85 247 / var(--tw-bg-opacity))}.bg-red-400{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity))}.bg-red-50\/20{background-color:#fef2f233}.bg-red-50\/30{background-color:#fef2f24d}.bg-red-50\/50{background-color:#fef2f280}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity))}.bg-red-500\/20{background-color:#ef444433}.bg-slate-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.bg-slate-200{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity))}.bg-slate-50{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity))}.bg-slate-50\/30{background-color:#f8fafc4d}.bg-slate-50\/50{background-color:#f8fafc80}.bg-slate-500\/70{background-color:#64748bb3}.bg-violet-100{--tw-bg-opacity: 1;background-color:rgb(237 233 254 / var(--tw-bg-opacity))}.bg-violet-200{--tw-bg-opacity: 1;background-color:rgb(221 214 254 / var(--tw-bg-opacity))}.bg-violet-300{--tw-bg-opacity: 1;background-color:rgb(196 181 253 / var(--tw-bg-opacity))}.bg-violet-500{--tw-bg-opacity: 1;background-color:rgb(139 92 246 / var(--tw-bg-opacity))}.bg-violet-500\/90{background-color:#8b5cf6e6}.bg-violet-700{--tw-bg-opacity: 1;background-color:rgb(109 40 217 / var(--tw-bg-opacity))}.bg-violet-800{--tw-bg-opacity: 1;background-color:rgb(91 33 182 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-white\/20{background-color:#fff3}.bg-white\/30{background-color:#ffffff4d}.bg-white\/50{background-color:#ffffff80}.bg-white\/80{background-color:#fffc}.bg-white\/90{background-color:#ffffffe6}.bg-yellow-400{--tw-bg-opacity: 1;background-color:rgb(250 204 21 / var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity: 1;background-color:rgb(254 252 232 / var(--tw-bg-opacity))}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity))}.bg-opacity-0{--tw-bg-opacity: 0}.bg-opacity-30{--tw-bg-opacity: .3}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-tr{background-image:linear-gradient(to top right,var(--tw-gradient-stops))}.from-\[\#ffffff00\]{--tw-gradient-from: #ffffff00 var(--tw-gradient-from-position);--tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-fuchsia-500{--tw-gradient-from: #d946ef var(--tw-gradient-from-position);--tw-gradient-to: rgb(217 70 239 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-indigo-400{--tw-gradient-from: #818cf8 var(--tw-gradient-from-position);--tw-gradient-to: rgb(129 140 248 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-indigo-500{--tw-gradient-from: #6366f1 var(--tw-gradient-from-position);--tw-gradient-to: rgb(99 102 241 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-indigo-600{--tw-gradient-from: #4f46e5 var(--tw-gradient-from-position);--tw-gradient-to: rgb(79 70 229 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-violet-500{--tw-gradient-from: #8b5cf6 var(--tw-gradient-from-position);--tw-gradient-to: rgb(139 92 246 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-white{--tw-gradient-from: #fff var(--tw-gradient-from-position);--tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.via-white{--tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), #fff var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-fuchsia-400{--tw-gradient-to: #e879f9 var(--tw-gradient-to-position)}.to-fuchsia-500{--tw-gradient-to: #d946ef var(--tw-gradient-to-position)}.to-fuchsia-600{--tw-gradient-to: #c026d3 var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to: transparent var(--tw-gradient-to-position)}.to-violet-500{--tw-gradient-to: #8b5cf6 var(--tw-gradient-to-position)}.bg-clip-text{background-clip:text}.object-cover{object-fit:cover}.object-center{object-position:center}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-10{padding:2.5rem}.p-12{padding:3rem}.p-16{padding:4rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-3\.5{padding:.875rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.\!py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-12{padding-left:3rem;padding-right:3rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-7{padding-left:1.75rem;padding-right:1.75rem}.px-8{padding-left:2rem;padding-right:2rem}.px-\[12px\]{padding-left:12px;padding-right:12px}.px-\[14px\]{padding-left:14px;padding-right:14px}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-\[2\.5px\]{padding-top:2.5px;padding-bottom:2.5px}.py-\[2px\]{padding-top:2px;padding-bottom:2px}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pb-px{padding-bottom:1px}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pl-8{padding-left:2rem}.pr-2{padding-right:.5rem}.pr-4{padding-right:1rem}.pt-0{padding-top:0}.pt-3{padding-top:.75rem}.pt-8{padding-top:2rem}.text-left{text-align:left}.text-center{text-align:center}.font-inter{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.font-lato{font-family:lato,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-6xl{font-size:3.75rem;line-height:1}.text-\[13px\]{font-size:13px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-5{line-height:1.25rem}.leading-\[1\.25em\]{line-height:1.25em}.leading-tight{line-height:1.25}.tracking-\[8px\]{letter-spacing:8px}.tracking-wider{letter-spacing:.05em}.\!text-slate-600{--tw-text-opacity: 1 !important;color:rgb(71 85 105 / var(--tw-text-opacity))!important}.\!text-violet-700{--tw-text-opacity: 1 !important;color:rgb(109 40 217 / var(--tw-text-opacity))!important}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.text-black\/10{color:#0000001a}.text-black\/20{color:#0003}.text-black\/30{color:#0000004d}.text-black\/50{color:#00000080}.text-black\/70{color:#000000b3}.text-black\/80{color:#000c}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity))}.text-fuchsia-400{--tw-text-opacity: 1;color:rgb(232 121 249 / var(--tw-text-opacity))}.text-fuchsia-600{--tw-text-opacity: 1;color:rgb(192 38 211 / var(--tw-text-opacity))}.text-fuchsia-800{--tw-text-opacity: 1;color:rgb(134 25 143 / var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-gray-600\/80{color:#4b5563cc}.text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity))}.text-orange-300{--tw-text-opacity: 1;color:rgb(253 186 116 / var(--tw-text-opacity))}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity))}.text-red-700\/70{color:#b91c1cb3}.text-slate-100{--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}.text-slate-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.text-slate-400\/80{color:#94a3b8cc}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}.text-slate-600{--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity))}.text-slate-700{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity))}.text-slate-800{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity))}.text-slate-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity))}.text-transparent{color:transparent}.text-violet-500{--tw-text-opacity: 1;color:rgb(139 92 246 / var(--tw-text-opacity))}.text-violet-500\/80{color:#8b5cf6cc}.text-violet-600{--tw-text-opacity: 1;color:rgb(124 58 237 / var(--tw-text-opacity))}.text-violet-700{--tw-text-opacity: 1;color:rgb(109 40 217 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-white\/70{color:#ffffffb3}.text-white\/80{color:#fffc}.text-yellow-200{--tw-text-opacity: 1;color:rgb(254 240 138 / var(--tw-text-opacity))}.text-yellow-600{--tw-text-opacity: 1;color:rgb(202 138 4 / var(--tw-text-opacity))}.text-opacity-90{--tw-text-opacity: .9}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-80{opacity:.8}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-black\/5{--tw-shadow-color: rgb(0 0 0 / .05);--tw-shadow: var(--tw-shadow-colored)}.shadow-slate-300\/30{--tw-shadow-color: rgb(203 213 225 / .3);--tw-shadow: var(--tw-shadow-colored)}.shadow-slate-300\/50{--tw-shadow-color: rgb(203 213 225 / .5);--tw-shadow: var(--tw-shadow-colored)}.shadow-slate-500\/50{--tw-shadow-color: rgb(100 116 139 / .5);--tw-shadow: var(--tw-shadow-colored)}.shadow-violet-200\/80{--tw-shadow-color: rgb(221 214 254 / .8);--tw-shadow: var(--tw-shadow-colored)}.shadow-violet-500\/40{--tw-shadow-color: rgb(139 92 246 / .4);--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-0{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-slate-200{--tw-ring-opacity: 1;--tw-ring-color: rgb(226 232 240 / var(--tw-ring-opacity))}.ring-transparent{--tw-ring-color: transparent}.ring-violet-500{--tw-ring-opacity: 1;--tw-ring-color: rgb(139 92 246 / var(--tw-ring-opacity))}.ring-offset-0{--tw-ring-offset-width: 0px}.ring-offset-violet-500{--tw-ring-offset-color: #8b5cf6}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur: blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-lg{--tw-backdrop-blur: blur(16px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-md{--tw-backdrop-blur: blur(12px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-none{--tw-backdrop-blur: blur(0);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur: blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[backdrop-filter\,background-color\]{transition-property:background-color,-webkit-backdrop-filter;transition-property:backdrop-filter,background-color;transition-property:backdrop-filter,background-color,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[background-color\,border-color\,color\]{transition-property:background-color,border-color,color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[background-color\,box-shadow\,transform\]{transition-property:background-color,box-shadow,transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[background-color\,box-shadow\]{transition-property:background-color,box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[background-color\,opacity\,transform\]{transition-property:background-color,opacity,transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[background-color\,transform\,box-shadow\]{transition-property:background-color,transform,box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[background-color\,transform\]{transition-property:background-color,transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[background-color\]{transition-property:background-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[border-color\,background-color\]{transition-property:border-color,background-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[border-color\,ring-color\]{transition-property:border-color,ring-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[border-color\,transform\,box-shadow\]{transition-property:border-color,transform,box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[box-shadow\,transform\,background-color\]{transition-property:box-shadow,transform,background-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[color\,transform\,opacity\]{transition-property:color,transform,opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[height\]{transition-property:height;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[left\,opacity\]{transition-property:left,opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[opacity\,backdrop-filter\]{transition-property:opacity,-webkit-backdrop-filter;transition-property:opacity,backdrop-filter;transition-property:opacity,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[opacity\,display\,background-color\]{transition-property:opacity,display,background-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[opacity\,left\]{transition-property:opacity,left;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[opacity\,transform\]{transition-property:opacity,transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[outline\,background-color\]{transition-property:outline,background-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[padding-bottom\,border-color\]{transition-property:padding-bottom,border-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[top\]{transition-property:top;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[transform\,background-color\,box-shadow\]{transition-property:transform,background-color,box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[transform\,border-color\,border\,background-color\]{transition-property:transform,border-color,border,background-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[transform\,box-shadow\]{transition-property:transform,box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[transform\,opacity\]{transition-property:transform,opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[width\]{transition-property:width;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.delay-500{transition-delay:.5s}.delay-\[1\.3s\]{transition-delay:1.3s}.delay-\[1\.6s\]{transition-delay:1.6s}.delay-\[1\.9s\]{transition-delay:1.9s}.delay-\[1s\]{transition-delay:1s}.delay-\[3\.5s\]{transition-delay:3.5s}.delay-\[4s\]{transition-delay:4s}.duration-100{transition-duration:.1s}.duration-1000{transition-duration:1s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.duration-700{transition-duration:.7s}.duration-75{transition-duration:75ms}.duration-\[1s\]{transition-duration:1s}.duration-\[2s\]{transition-duration:2s}.duration-\[5s\]{transition-duration:5s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.\[-webkit-background-clip\:text\;\],.\[-webkit-background-clip\:text\]{-webkit-background-clip:text}.\[backface-visibility\:hidden\]{-webkit-backface-visibility:hidden;backface-visibility:hidden}.\[background\:radial-gradient\(\#8b5cf656_0\%\,transparent_70\%\,transparent\)\]{background:radial-gradient(#8b5cf656 0%,transparent 70%,transparent)}.\[background\:radial-gradient\(\#d946ef56_0\%\,transparent_70\%\,transparent\)\]{background:radial-gradient(#d946ef56 0%,transparent 70%,transparent)}.\[perspective\:400px\]{perspective:400px}.\[transform-style\:preserve-3d\]{transform-style:preserve-3d}.\[transform\:rotateY\(-180deg\)\]{transform:rotateY(-180deg)}.\[transform\:rotateY\(180deg\)\]{transform:rotateY(180deg)}*{touch-callout:none;user-select:none;-webkit-touch-callout:none;-webkit-user-select:none}input,textarea{touch-callout:default;user-select:auto;-webkit-touch-callout:default;-webkit-user-select:auto}.dark *::-webkit-scrollbar,body.dark::-webkit-scrollbar{width:12px}.dark *::-webkit-scrollbar-track,body.dark::-webkit-scrollbar-track{background-color:#0f172a}.dark *::-webkit-scrollbar-thumb,body.dark::-webkit-scrollbar-thumb{background-color:#1e293b;border-radius:6px}.dark *::-webkit-scrollbar-thumb:hover,body.dark::-webkit-scrollbar-thumb{background-color:#334155}.placeholder\:text-slate-200::placeholder{--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity))}.placeholder\:text-slate-400::placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.placeholder\:text-slate-400\/90::placeholder{color:#94a3b8e6}.placeholder\:antialiased::placeholder{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.before\:\[content\:\'-_\'\]:before{content:"- "}.after\:\[content\:\'_-\'\]:after{content:" -"}.even\:bg-slate-50:nth-child(even){--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity))}.even\:text-slate-50:nth-child(even){--tw-text-opacity: 1;color:rgb(248 250 252 / var(--tw-text-opacity))}.hover\:-translate-y-0:hover{--tw-translate-y: -0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:-translate-y-0\.5:hover{--tw-translate-y: -.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:-translate-y-\[1px\]:hover{--tw-translate-y: -1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-105:hover{--tw-scale-x: 1.05;--tw-scale-y: 1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-\[102\%\]:hover{--tw-scale-x: 102%;--tw-scale-y: 102%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-slate-400:hover{--tw-border-opacity: 1;border-color:rgb(148 163 184 / var(--tw-border-opacity))}.hover\:border-slate-600:hover{--tw-border-opacity: 1;border-color:rgb(71 85 105 / var(--tw-border-opacity))}.hover\:border-violet-200:hover{--tw-border-opacity: 1;border-color:rgb(221 214 254 / var(--tw-border-opacity))}.hover\:border-violet-50:hover{--tw-border-opacity: 1;border-color:rgb(245 243 255 / var(--tw-border-opacity))}.hover\:border-violet-800:hover{--tw-border-opacity: 1;border-color:rgb(91 33 182 / var(--tw-border-opacity))}.hover\:\!bg-violet-200:hover{--tw-bg-opacity: 1 !important;background-color:rgb(221 214 254 / var(--tw-bg-opacity))!important}.hover\:bg-red-100:hover{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity))}.hover\:bg-slate-100:hover{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.hover\:bg-slate-200:hover{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity))}.hover\:bg-slate-300:hover{--tw-bg-opacity: 1;background-color:rgb(203 213 225 / var(--tw-bg-opacity))}.hover\:bg-slate-50:hover{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity))}.hover\:bg-slate-600\/70:hover{background-color:#475569b3}.hover\:bg-violet-100:hover{--tw-bg-opacity: 1;background-color:rgb(237 233 254 / var(--tw-bg-opacity))}.hover\:bg-violet-200:hover{--tw-bg-opacity: 1;background-color:rgb(221 214 254 / var(--tw-bg-opacity))}.hover\:bg-violet-400:hover{--tw-bg-opacity: 1;background-color:rgb(167 139 250 / var(--tw-bg-opacity))}.hover\:bg-violet-50:hover{--tw-bg-opacity: 1;background-color:rgb(245 243 255 / var(--tw-bg-opacity))}.hover\:bg-violet-600\/90:hover{background-color:#7c3aede6}.hover\:bg-violet-800:hover{--tw-bg-opacity: 1;background-color:rgb(91 33 182 / var(--tw-bg-opacity))}.hover\:bg-white\/100:hover{background-color:#fff}.hover\:bg-white\/20:hover{background-color:#fff3}.hover\:bg-white\/30:hover{background-color:#ffffff4d}.hover\:bg-white\/80:hover{background-color:#fffc}.hover\:from-indigo-600:hover{--tw-gradient-from: #4f46e5 var(--tw-gradient-from-position);--tw-gradient-to: rgb(79 70 229 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.hover\:to-fuchsia-600:hover{--tw-gradient-to: #c026d3 var(--tw-gradient-to-position)}.hover\:pb-0:hover{padding-bottom:0}.hover\:pb-0\.5:hover{padding-bottom:.125rem}.hover\:text-black:hover{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity))}.hover\:text-black\/30:hover{color:#0000004d}.hover\:text-black\/40:hover{color:#0006}.hover\:text-red-700:hover{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity))}.hover\:text-slate-600:hover{--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity))}.hover\:opacity-90:hover{opacity:.9}.hover\:shadow-2xl:hover{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-xl:hover{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-slate-300\/60:hover{--tw-shadow-color: rgb(203 213 225 / .6);--tw-shadow: var(--tw-shadow-colored)}.hover\:shadow-violet-500\/50:hover{--tw-shadow-color: rgb(139 92 246 / .5);--tw-shadow: var(--tw-shadow-colored)}.focus\:z-10:focus{z-index:10}.focus\:-translate-y-0:focus{--tw-translate-y: -0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.focus\:-translate-y-0\.5:focus{--tw-translate-y: -.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.focus\:border-indigo-200:focus{--tw-border-opacity: 1;border-color:rgb(199 210 254 / var(--tw-border-opacity))}.focus\:border-indigo-500:focus{--tw-border-opacity: 1;border-color:rgb(99 102 241 / var(--tw-border-opacity))}.focus\:border-red-500:focus{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity))}.focus\:border-violet-400:focus{--tw-border-opacity: 1;border-color:rgb(167 139 250 / var(--tw-border-opacity))}.focus\:bg-slate-50:focus{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity))}.focus\:shadow-md:focus{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:shadow-xl:focus{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:shadow-slate-300\/50:focus{--tw-shadow-color: rgb(203 213 225 / .5);--tw-shadow: var(--tw-shadow-colored)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-0:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-indigo-400\/50:focus{--tw-ring-color: rgb(129 140 248 / .5)}.focus\:ring-indigo-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity))}.focus\:ring-red-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity))}.focus\:ring-slate-200:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(226 232 240 / var(--tw-ring-opacity))}.focus\:ring-violet-300:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(196 181 253 / var(--tw-ring-opacity))}.focus\:ring-violet-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(139 92 246 / var(--tw-ring-opacity))}.focus\:ring-violet-700:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(109 40 217 / var(--tw-ring-opacity))}.focus\:ring-white:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity))}.focus\:\!ring-offset-0:focus{--tw-ring-offset-width: 0px !important}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus\:ring-offset-slate-50:focus{--tw-ring-offset-color: #f8fafc}.active\:translate-y-0:active{--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:scale-95:active{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:scale-\[0\.98\]:active{--tw-scale-x: .98;--tw-scale-y: .98;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:scale-\[97\%\]:active{--tw-scale-x: 97%;--tw-scale-y: 97%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:scale-\[98\%\]:active{--tw-scale-x: 98%;--tw-scale-y: 98%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:bg-violet-200:active{--tw-bg-opacity: 1;background-color:rgb(221 214 254 / var(--tw-bg-opacity))}.active\:shadow:active{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.active\:shadow-lg:active{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.active\:shadow-md:active{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.active\:shadow-violet-500\/30:active{--tw-shadow-color: rgb(139 92 246 / .3);--tw-shadow: var(--tw-shadow-colored)}.group:nth-child(odd) .group-odd\:bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.group:nth-child(odd) .group-odd\:to-white{--tw-gradient-to: #fff var(--tw-gradient-to-position)}.group:nth-child(even) .group-even\:bg-slate-50{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity))}.group:nth-child(even) .group-even\:to-slate-50{--tw-gradient-to: #f8fafc var(--tw-gradient-to-position)}.group:hover .group-hover\:bg-slate-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity))}.group:hover .group-hover\:to-slate-100{--tw-gradient-to: #f1f5f9 var(--tw-gradient-to-position)}:is(.dark .dark\:border){border-width:1px}:is(.dark .dark\:\!border-violet-600){--tw-border-opacity: 1 !important;border-color:rgb(124 58 237 / var(--tw-border-opacity))!important}:is(.dark .dark\:border-blue-500\/50){border-color:#3b82f680}:is(.dark .dark\:border-fuchsia-400){--tw-border-opacity: 1;border-color:rgb(232 121 249 / var(--tw-border-opacity))}:is(.dark .dark\:border-green-500\/50){border-color:#22c55e80}:is(.dark .dark\:border-green-500\/70){border-color:#22c55eb3}:is(.dark .dark\:border-indigo-400){--tw-border-opacity: 1;border-color:rgb(129 140 248 / var(--tw-border-opacity))}:is(.dark .dark\:border-red-500){--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity))}:is(.dark .dark\:border-red-500\/20){border-color:#ef444433}:is(.dark .dark\:border-red-500\/40){border-color:#ef444466}:is(.dark .dark\:border-red-500\/50){border-color:#ef444480}:is(.dark .dark\:border-red-500\/70){border-color:#ef4444b3}:is(.dark .dark\:border-red-600\/50){border-color:#dc262680}:is(.dark .dark\:border-red-700\/50){border-color:#b91c1c80}:is(.dark .dark\:border-slate-500){--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity))}:is(.dark .dark\:border-slate-600){--tw-border-opacity: 1;border-color:rgb(71 85 105 / var(--tw-border-opacity))}:is(.dark .dark\:border-slate-700){--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity))}:is(.dark .dark\:border-slate-700\/50){border-color:#33415580}:is(.dark .dark\:border-slate-700\/70){border-color:#334155b3}:is(.dark .dark\:border-slate-800){--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity))}:is(.dark .dark\:border-transparent){border-color:transparent}:is(.dark .dark\:border-violet-700){--tw-border-opacity: 1;border-color:rgb(109 40 217 / var(--tw-border-opacity))}:is(.dark .dark\:border-white\/20){border-color:#fff3}:is(.dark .dark\:border-yellow-500\/50){border-color:#eab30880}:is(.dark .dark\:border-yellow-500\/70){border-color:#eab308b3}:is(.dark .dark\:\!bg-violet-700\/60){background-color:#6d28d999!important}:is(.dark .dark\:bg-black\/20){background-color:#0003}:is(.dark .dark\:bg-black\/50){background-color:#00000080}:is(.dark .dark\:bg-blue-500\/10){background-color:#3b82f61a}:is(.dark .dark\:bg-fuchsia-500\/20){background-color:#d946ef33}:is(.dark .dark\:bg-green-500\/10){background-color:#22c55e1a}:is(.dark .dark\:bg-green-500\/30){background-color:#22c55e4d}:is(.dark .dark\:bg-indigo-500\/20){background-color:#6366f133}:is(.dark .dark\:bg-indigo-600\/50){background-color:#4f46e580}:is(.dark .dark\:bg-purple-800){--tw-bg-opacity: 1;background-color:rgb(107 33 168 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-500\/10){background-color:#ef44441a}:is(.dark .dark\:bg-red-500\/20){background-color:#ef444433}:is(.dark .dark\:bg-red-500\/30){background-color:#ef44444d}:is(.dark .dark\:bg-red-500\/5){background-color:#ef44440d}:is(.dark .dark\:bg-red-600\/5){background-color:#dc26260d}:is(.dark .dark\:bg-slate-200){--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-slate-700){--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-slate-700\/20){background-color:#33415533}:is(.dark .dark\:bg-slate-700\/50){background-color:#33415580}:is(.dark .dark\:bg-slate-800){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-slate-800\/30){background-color:#1e293b4d}:is(.dark .dark\:bg-slate-800\/50){background-color:#1e293b80}:is(.dark .dark\:bg-slate-800\/60){background-color:#1e293b99}:is(.dark .dark\:bg-slate-800\/80){background-color:#1e293bcc}:is(.dark .dark\:bg-slate-900){--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-slate-900\/50){background-color:#0f172a80}:is(.dark .dark\:bg-violet-600){--tw-bg-opacity: 1;background-color:rgb(124 58 237 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-violet-700){--tw-bg-opacity: 1;background-color:rgb(109 40 217 / var(--tw-bg-opacity))}:is(.dark .dark\:bg-violet-700\/90){background-color:#6d28d9e6}:is(.dark .dark\:bg-white\/20){background-color:#fff3}:is(.dark .dark\:bg-white\/5){background-color:#ffffff0d}:is(.dark .dark\:bg-white\/80){background-color:#fffc}:is(.dark .dark\:bg-yellow-500\/10){background-color:#eab3081a}:is(.dark .dark\:bg-yellow-500\/30){background-color:#eab3084d}:is(.dark .dark\:bg-opacity-0){--tw-bg-opacity: 0}:is(.dark .dark\:bg-opacity-90){--tw-bg-opacity: .9}:is(.dark .dark\:from-\[\#0f172a00\]){--tw-gradient-from: #0f172a00 var(--tw-gradient-from-position);--tw-gradient-to: rgb(15 23 42 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}:is(.dark .dark\:\!text-violet-100){--tw-text-opacity: 1 !important;color:rgb(237 233 254 / var(--tw-text-opacity))!important}:is(.dark .dark\:text-blue-300){--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity))}:is(.dark .dark\:text-fuchsia-200){--tw-text-opacity: 1;color:rgb(245 208 254 / var(--tw-text-opacity))}:is(.dark .dark\:text-green-300){--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity))}:is(.dark .dark\:text-indigo-200){--tw-text-opacity: 1;color:rgb(199 210 254 / var(--tw-text-opacity))}:is(.dark .dark\:text-indigo-300){--tw-text-opacity: 1;color:rgb(165 180 252 / var(--tw-text-opacity))}:is(.dark .dark\:text-red-100){--tw-text-opacity: 1;color:rgb(254 226 226 / var(--tw-text-opacity))}:is(.dark .dark\:text-red-200){--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}:is(.dark .dark\:text-red-300){--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity))}:is(.dark .dark\:text-red-400){--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity))}:is(.dark .dark\:text-red-400\/50){color:#f8717180}:is(.dark .dark\:text-slate-100){--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}:is(.dark .dark\:text-slate-200){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity))}:is(.dark .dark\:text-slate-300){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity))}:is(.dark .dark\:text-slate-400){--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}:is(.dark .dark\:text-slate-500){--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}:is(.dark .dark\:text-slate-600){--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity))}:is(.dark .dark\:text-slate-700){--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity))}:is(.dark .dark\:text-slate-900){--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity))}:is(.dark .dark\:text-violet-400){--tw-text-opacity: 1;color:rgb(167 139 250 / var(--tw-text-opacity))}:is(.dark .dark\:text-white){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}:is(.dark .dark\:text-white\/10){color:#ffffff1a}:is(.dark .dark\:text-white\/20){color:#fff3}:is(.dark .dark\:text-white\/30){color:#ffffff4d}:is(.dark .dark\:text-white\/50){color:#ffffff80}:is(.dark .dark\:text-white\/60){color:#fff9}:is(.dark .dark\:text-white\/70){color:#ffffffb3}:is(.dark .dark\:text-white\/80){color:#fffc}:is(.dark .dark\:text-yellow-100){--tw-text-opacity: 1;color:rgb(254 249 195 / var(--tw-text-opacity))}:is(.dark .dark\:text-yellow-200){--tw-text-opacity: 1;color:rgb(254 240 138 / var(--tw-text-opacity))}:is(.dark .dark\:text-yellow-300){--tw-text-opacity: 1;color:rgb(253 224 71 / var(--tw-text-opacity))}:is(.dark .dark\:opacity-90){opacity:.9}:is(.dark .dark\:shadow-lg){--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}:is(.dark .dark\:shadow-black\/20){--tw-shadow-color: rgb(0 0 0 / .2);--tw-shadow: var(--tw-shadow-colored)}:is(.dark .dark\:shadow-black\/30){--tw-shadow-color: rgb(0 0 0 / .3);--tw-shadow: var(--tw-shadow-colored)}:is(.dark .dark\:shadow-black\/50){--tw-shadow-color: rgb(0 0 0 / .5);--tw-shadow: var(--tw-shadow-colored)}:is(.dark .dark\:ring-offset-slate-900){--tw-ring-offset-color: #0f172a}:is(.dark .dark\:backdrop-blur-sm){--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}:is(.dark .dark\:\[filter\:brightness\(600\%\)\]){filter:brightness(600%)}:is(.dark .dark\:placeholder\:text-slate-500)::placeholder{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}:is(.dark .dark\:even\:bg-\[\#141A2F\]:nth-child(even)){--tw-bg-opacity: 1;background-color:rgb(20 26 47 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:border-slate-100:hover){--tw-border-opacity: 1;border-color:rgb(241 245 249 / var(--tw-border-opacity))}:is(.dark .dark\:hover\:border-slate-500:hover){--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity))}:is(.dark .dark\:hover\:border-violet-700:hover){--tw-border-opacity: 1;border-color:rgb(109 40 217 / var(--tw-border-opacity))}:is(.dark .dark\:hover\:\!bg-violet-700:hover){--tw-bg-opacity: 1 !important;background-color:rgb(109 40 217 / var(--tw-bg-opacity))!important}:is(.dark .dark\:hover\:bg-red-500\/20:hover){background-color:#ef444433}:is(.dark .dark\:hover\:bg-slate-600:hover){--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-slate-700\/80:hover){background-color:#334155cc}:is(.dark .dark\:hover\:bg-slate-800:hover){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-slate-800\/50:hover){background-color:#1e293b80}:is(.dark .dark\:hover\:bg-violet-700:hover){--tw-bg-opacity: 1;background-color:rgb(109 40 217 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-violet-800\/90:hover){background-color:#5b21b6e6}:is(.dark .dark\:hover\:bg-white:hover){--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-white\/10:hover){background-color:#ffffff1a}:is(.dark .dark\:hover\:bg-white\/30:hover){background-color:#ffffff4d}:is(.dark .hover\:dark\:bg-slate-900\/80):hover{background-color:#0f172acc}:is(.dark .dark\:hover\:text-red-200:hover){--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-slate-400:hover){--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-white:hover){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}:is(.dark .hover\:dark\:text-white\/30):hover{color:#ffffff4d}:is(.dark .hover\:dark\:text-white\/40):hover{color:#fff6}:is(.dark .dark\:focus\:bg-slate-800:focus){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}:is(.dark .dark\:focus\:ring-indigo-500\/70:focus){--tw-ring-color: rgb(99 102 241 / .7)}:is(.dark .group:nth-child(odd) .dark\:group-odd\:bg-slate-900){--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity))}:is(.dark .group:nth-child(odd) .dark\:group-odd\:to-slate-900){--tw-gradient-to: #0f172a var(--tw-gradient-to-position)}:is(.dark .group:nth-child(even) .dark\:group-even\:bg-\[\#141A2E\]){--tw-bg-opacity: 1;background-color:rgb(20 26 46 / var(--tw-bg-opacity))}:is(.dark .group:nth-child(even) .dark\:group-even\:to-\[\#141A2E\]){--tw-gradient-to: #141A2E var(--tw-gradient-to-position)}:is(.dark .group:hover .dark\:group-hover\:bg-slate-800){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity))}:is(.dark .group:hover .dark\:group-hover\:to-slate-800){--tw-gradient-to: #1e293b var(--tw-gradient-to-position)}@media (min-width: 500px){.xs\:flex{display:flex}.xs\:rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}}@media (min-width: 640px){.sm\:py-2{padding-top:.5rem;padding-bottom:.5rem}.sm\:py-2\.5{padding-top:.625rem;padding-bottom:.625rem}} diff --git a/macapp/Xcode/Gertrude/WebViews/BlockedRequests/index.js b/macapp/Xcode/Gertrude/WebViews/BlockedRequests/index.js index 555d8ef9..07faf305 100644 --- a/macapp/Xcode/Gertrude/WebViews/BlockedRequests/index.js +++ b/macapp/Xcode/Gertrude/WebViews/BlockedRequests/index.js @@ -68,7 +68,7 @@ Error generating stack: `+o.message+` * LICENSE.md file in the root directory of this source tree. * * @license MIT - */function si(){return si=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&(n[l]=e[l]);return n}function e0(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function t0(e,t){return e.button===0&&(!t||t==="_self")&&!e0(e)}const n0=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset"],r0=E.forwardRef(function(t,n){let{onClick:r,relative:l,reloadDocument:o,replace:i,state:u,target:s,to:a,preventScrollReset:h}=t,d=bp(t,n0),m=Jp(a,{relative:l}),y=l0(a,{replace:i,state:u,target:s,preventScrollReset:h,relative:l});function w(k){r&&r(k),k.defaultPrevented||y(k)}return E.createElement("a",si({},d,{href:m,onClick:o?r:w,ref:n,target:s}))});var _s;(function(e){e.UseScrollRestoration="useScrollRestoration",e.UseSubmitImpl="useSubmitImpl",e.UseFetcher="useFetcher"})(_s||(_s={}));var Ps;(function(e){e.UseFetchers="useFetchers",e.UseScrollRestoration="useScrollRestoration"})(Ps||(Ps={}));function l0(e,t){let{target:n,replace:r,state:l,preventScrollReset:o,relative:i}=t===void 0?{}:t,u=qp(),s=iu(),a=Kc(e,{relative:i});return E.useCallback(h=>{if(t0(h,n)){h.preventDefault();let d=r!==void 0?r:Ss(s)===Ss(a);u(e,{replace:d,state:l,preventScrollReset:o,relative:i})}},[s,u,a,r,l,n,e,o,i])}const dn=({size:e="medium",fullWidth:t=!1,testId:n,color:r,className:l,disabled:o=!1,...i})=>{let u="";if(o)u="bg-slate-50 dark:bg-black/50 text-slate-400 dark:text-slate-600 border border-slate-200 dark:border-slate-800 cursor-not-allowed ring-transparent focus:ring-slate-200";else switch(r){case"primary-on-violet-bg":u="bg-white text-violet-500 hover:bg-violet-50 border-2 border-white hover:border-violet-50 ring-violet-500 ring-offset-violet-500 focus:ring-white";break;case"secondary-on-violet-bg":u="bg-violet-500 text-white border-2 border-white hover:bg-violet-400 ring-violet-500 focus:ring-white ring-offset-violet-500";break;case"primary":u="bg-violet-700 dark:bg-violet-700 border border-violet-700 dark:border-violet-700 hover:border-violet-800 dark:hover:border-violet-700 text-white dark:hover:bg-violet-700 hover:bg-violet-800 ring-transparent focus:ring-violet-700 dark:ring-offset-slate-900";break;case"secondary":u="bg-violet-100 dark:bg-slate-800/80 border border-violet-100 dark:border-slate-700/70 hover:border-violet-200 dark:hover:bg-slate-700/80 text-violet-600 dark:text-white/80 hover:bg-violet-200 ring-transparent focus:ring-violet-300 dark:focus:ring-indigo-500/70 dark:ring-offset-slate-900";break;case"tertiary":u="bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 ring-transparent focus:ring-indigo-400/50 focus:border-indigo-200 dark:ring-offset-slate-900";break;case"warning":u="bg-red-50 dark:bg-red-500/10 text-red-600 dark:text-red-300 border-red-100 dark:border-red-600/50 border hover:text-red-700 hover:bg-red-100 dark:hover:bg-red-500/20 dark:hover:text-red-200 ring-transparent focus:ring-red-500 focus:border-red-500 dark:ring-offset-slate-900";break}const s=i.type==="button"||i.type==="submit";let a="";switch(e){case"small":a="text-base px-4 py-1.5 font-semibold";break;case"medium":a="text-base px-5 py-3 sm:py-2.5";break;default:a="text-lg px-10 py-2.5";break}const h=J(u,"ring ring-offset-0 focus:ring-offset-2 rounded-xl font-bold transition duration-100 outline-none block",a,t?"w-full":"w-auto",!s&&"text-center",l);return s?S("button",{type:i.type,className:h,disabled:o,...n?{"data-test":n}:{},...i.type==="button"?{onClick:o?()=>{}:()=>i.onClick()}:{},children:i.children}):i.type==="external"?S("a",{className:h,...n?{"data-test":n}:{},...o?{onClick:d=>d.preventDefault()}:{href:i.href},children:i.children}):S(r0,{className:h,to:o?"#":i.to,...n?{"data-test":n}:{},onClick:o?d=>d.preventDefault():()=>{},children:i.children})},o0=({children:e,htmlFor:t,className:n})=>S("label",{htmlFor:t,className:J("text-left text-slate-500 dark:text-slate-300 font-semibold text-md mb-1",n),children:e});function i0(e){return u0(new Date(e),"dateInput")}function u0(e,t){return t==="short"?e.toLocaleDateString():t==="url"?[`${e.getMonth()+1}`.padStart(2,"0"),`${e.getDate()}`.padStart(2,"0"),`${e.getFullYear()}`].join("-"):t==="dateInput"?[`${e.getFullYear()}`,`${e.getMonth()+1}`.padStart(2,"0"),`${e.getDate()}`.padStart(2,"0")].join("-"):[e.toLocaleDateString("en-US",{weekday:"long"}),", ",e.toLocaleDateString("en-US",{month:t==="long"?"long":"short"})," ",e.getDate(),", ",e.getFullYear()].join("")}const s0=({label:e,optional:t,value:n,setValue:r,required:l=!1,autoFocus:o=!1,placeholder:i,className:u,disabled:s,name:a,testId:h,...d})=>{const[m,y]=E.useState(n),w=E.useId(),k=xt(d)?"input":"textarea";return O("div",{className:J("flex flex-col space-y-1 w-full",u),children:[(e||t)&&O("div",{className:"flex flex-row justify-between items-center",children:[e&&S(o0,{htmlFor:w,children:e}),t&&S("span",{className:"text-violet-500/80 translate-y-px text-sm antialiased italic",children:"*optional"})]}),O("div",{className:"flex shadow-sm rounded-lg",children:[xt(d)&&d.prefix&&S("div",{className:"hidden xs:flex justify-center items-center p-3 bg-slate-50 dark:bg-slate-700/50 border border-r-0 dark:border-slate-700 rounded-l-lg",children:S("h3",{className:"text-slate-500 dark:text-slate-400",children:d.prefix})}),S(k,{id:w,type:d.type==="positiveInteger"?"number":d.type,value:m,required:!!l,autoFocus:o,placeholder:i,disabled:s,name:a,...h?{"data-test":h}:{},...d.type==="url"?{autoCapitalize:"none",autoCorrect:"off"}:{},...d.type==="password"?{minLength:4}:{},...d.type==="date"?{min:i0(new Date().toISOString())}:{},...xt(d)?{}:{rows:d.rows},onChange:F=>{const f=F.target.value;y(f),(d.type!=="positiveInteger"||a0(f))&&r(f)},className:J("py-3 px-4 flex-grow w-12","border border-slate-200 rounded-lg","transition-[border-color,ring-color] duration-150","text-slate-600 placeholder:text-slate-400/90 placeholder:antialiased","ring-0 ring-slate-200 outline-none focus:shadow-md focus:border-indigo-500 focus:ring-indigo-500 focus:ring-1","dark:bg-slate-700/20 dark:border-slate-700 dark:placeholder:text-slate-500 dark:text-white",!xt(d)&&d.noResize&&"resize-none",xt(d)&&d.unit&&"rounded-r-none",xt(d)&&d.prefix&&"xs:rounded-l-none")}),xt(d)&&d.unit&&S("div",{className:"flex justify-center items-center p-3 bg-slate-50 dark:bg-slate-700/50 border border-l-0 dark:border-slate-700 rounded-r-lg",children:S("h3",{className:"text-slate-500 dark:text-slate-400",children:d.unit})})]})]})};function a0(e){return e.match(/^[0-9]+$/)!==null&&Number.isInteger(Number(e))&&Number(e)>=0}function xt(e){return e.type!=="textarea"}var c0=Object.defineProperty,f0=(e,t,n)=>t in e?c0(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,uo=(e,t,n)=>(f0(e,typeof t!="symbol"?t+"":t,n),n);let d0=class{constructor(){uo(this,"current",this.detect()),uo(this,"handoffState","pending"),uo(this,"currentId",0)}set(t){this.current!==t&&(this.handoffState="pending",this.currentId=0,this.current=t)}reset(){this.set(this.detect())}nextId(){return++this.currentId}get isServer(){return this.current==="server"}get isClient(){return this.current==="client"}detect(){return typeof window>"u"||typeof document>"u"?"server":"client"}handoff(){this.handoffState==="pending"&&(this.handoffState="complete")}get isHandoffComplete(){return this.handoffState==="complete"}},rn=new d0,pn=(e,t)=>{rn.isServer?E.useEffect(e,t):E.useLayoutEffect(e,t)};function p0(e){let t=E.useRef(e);return pn(()=>{t.current=e},[e]),t}function h0(e){typeof queueMicrotask=="function"?queueMicrotask(e):Promise.resolve().then(e).catch(t=>setTimeout(()=>{throw t}))}function m0(){let e=[],t=[],n={enqueue(r){t.push(r)},addEventListener(r,l,o,i){return r.addEventListener(l,o,i),n.add(()=>r.removeEventListener(l,o,i))},requestAnimationFrame(...r){let l=requestAnimationFrame(...r);return n.add(()=>cancelAnimationFrame(l))},nextFrame(...r){return n.requestAnimationFrame(()=>n.requestAnimationFrame(...r))},setTimeout(...r){let l=setTimeout(...r);return n.add(()=>clearTimeout(l))},microTask(...r){let l={current:!0};return h0(()=>{l.current&&r[0]()}),n.add(()=>{l.current=!1})},add(r){return e.push(r),()=>{let l=e.indexOf(r);if(l>=0){let[o]=e.splice(l,1);o()}}},dispose(){for(let r of e.splice(0))r()},async workQueue(){for(let r of t.splice(0))await r()},style(r,l,o){let i=r.style.getPropertyValue(l);return Object.assign(r.style,{[l]:o}),this.add(()=>{Object.assign(r.style,{[l]:i})})}};return n}function v0(){let[e]=E.useState(m0);return E.useEffect(()=>()=>e.dispose(),[e]),e}let it=function(e){let t=p0(e);return _e.useCallback((...n)=>t.current(...n),[t])};function g0(){let[e,t]=E.useState(rn.isHandoffComplete);return e&&rn.isHandoffComplete===!1&&t(!1),E.useEffect(()=>{e!==!0&&t(!0)},[e]),E.useEffect(()=>rn.handoff(),[]),e}var Ts;let uu=(Ts=_e.useId)!=null?Ts:function(){let e=g0(),[t,n]=_e.useState(e?()=>rn.nextId():null);return pn(()=>{t===null&&n(rn.nextId())},[t]),t!=null?""+t:void 0};function Yc(e,t,...n){if(e in t){let l=t[e];return typeof l=="function"?l(...n):l}let r=new Error(`Tried to handle "${e}" but there is no handler defined. Only defined handlers are: ${Object.keys(t).map(l=>`"${l}"`).join(", ")}.`);throw Error.captureStackTrace&&Error.captureStackTrace(r,Yc),r}function Ls(e){var t;if(e.type)return e.type;let n=(t=e.as)!=null?t:"button";if(typeof n=="string"&&n.toLowerCase()==="button")return"button"}function y0(e,t){let[n,r]=E.useState(()=>Ls(e));return pn(()=>{r(Ls(e))},[e.type,e.as]),pn(()=>{n||!t.current||t.current instanceof HTMLButtonElement&&!t.current.hasAttribute("type")&&r("button")},[n,t]),n}let w0=Symbol();function su(...e){let t=E.useRef(e);E.useEffect(()=>{t.current=e},[e]);let n=it(r=>{for(let l of t.current)l!=null&&(typeof l=="function"?l(r):l.current=r)});return e.every(r=>r==null||r?.[w0])?void 0:n}function k0(...e){return e.filter(Boolean).join(" ")}var S0=(e=>(e[e.None=0]="None",e[e.RenderStrategy=1]="RenderStrategy",e[e.Static=2]="Static",e))(S0||{}),x0=(e=>(e[e.Unmount=0]="Unmount",e[e.Hidden=1]="Hidden",e))(x0||{});function ar({ourProps:e,theirProps:t,slot:n,defaultTag:r,features:l,visible:o=!0,name:i}){let u=Gc(t,e);if(o)return Lr(u,n,r,i);let s=l??0;if(s&2){let{static:a=!1,...h}=u;if(a)return Lr(h,n,r,i)}if(s&1){let{unmount:a=!0,...h}=u;return Yc(a?0:1,{[0](){return null},[1](){return Lr({...h,hidden:!0,style:{display:"none"}},n,r,i)}})}return Lr(u,n,r,i)}function Lr(e,t={},n,r){var l;let{as:o=n,children:i,refName:u="ref",...s}=so(e,["unmount","static"]),a=e.ref!==void 0?{[u]:e.ref}:{},h=typeof i=="function"?i(t):i;s.className&&typeof s.className=="function"&&(s.className=s.className(t));let d={};if(t){let m=!1,y=[];for(let[w,k]of Object.entries(t))typeof k=="boolean"&&(m=!0),k===!0&&y.push(w);m&&(d["data-headlessui-state"]=y.join(" "))}if(o===E.Fragment&&Object.keys(ai(s)).length>0){if(!E.isValidElement(h)||Array.isArray(h)&&h.length>1)throw new Error(['Passing props on "Fragment"!',"",`The current component <${r} /> is rendering a "Fragment".`,"However we need to passthrough the following props:",Object.keys(s).map(w=>` - ${w}`).join(` + */function si(){return si=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&(n[l]=e[l]);return n}function e0(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function t0(e,t){return e.button===0&&(!t||t==="_self")&&!e0(e)}const n0=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset"],r0=E.forwardRef(function(t,n){let{onClick:r,relative:l,reloadDocument:o,replace:i,state:u,target:s,to:a,preventScrollReset:h}=t,d=bp(t,n0),m=Jp(a,{relative:l}),y=l0(a,{replace:i,state:u,target:s,preventScrollReset:h,relative:l});function w(k){r&&r(k),k.defaultPrevented||y(k)}return E.createElement("a",si({},d,{href:m,onClick:o?r:w,ref:n,target:s}))});var _s;(function(e){e.UseScrollRestoration="useScrollRestoration",e.UseSubmitImpl="useSubmitImpl",e.UseFetcher="useFetcher"})(_s||(_s={}));var Ps;(function(e){e.UseFetchers="useFetchers",e.UseScrollRestoration="useScrollRestoration"})(Ps||(Ps={}));function l0(e,t){let{target:n,replace:r,state:l,preventScrollReset:o,relative:i}=t===void 0?{}:t,u=qp(),s=iu(),a=Kc(e,{relative:i});return E.useCallback(h=>{if(t0(h,n)){h.preventDefault();let d=r!==void 0?r:Ss(s)===Ss(a);u(e,{replace:d,state:l,preventScrollReset:o,relative:i})}},[s,u,a,r,l,n,e,o,i])}const dn=({size:e="medium",fullWidth:t=!1,testId:n,color:r,className:l,disabled:o=!1,...i})=>{let u="";if(o)u="bg-slate-50 dark:bg-black/50 text-slate-400 dark:text-slate-600 border border-slate-200 dark:border-slate-800 cursor-not-allowed ring-transparent focus:ring-slate-200";else switch(r){case"primary-on-violet-bg":u="bg-white text-violet-500 hover:bg-violet-50 border-2 border-white hover:border-violet-50 ring-violet-500 ring-offset-violet-500 focus:ring-white";break;case"secondary-on-violet-bg":u="bg-violet-500 text-white border-2 border-white hover:bg-violet-400 ring-violet-500 focus:ring-white ring-offset-violet-500";break;case"primary":u="bg-violet-700 dark:bg-violet-700 border border-violet-700 dark:border-violet-700 hover:border-violet-800 dark:hover:border-violet-700 text-white dark:hover:bg-violet-700 hover:bg-violet-800 ring-transparent focus:ring-violet-700 dark:ring-offset-slate-900";break;case"secondary":u="bg-violet-100 dark:bg-slate-800/80 border border-violet-100 dark:border-slate-700/70 hover:border-violet-200 dark:hover:bg-slate-700/80 text-violet-600 dark:text-white/80 hover:bg-violet-200 ring-transparent focus:ring-violet-300 dark:focus:ring-indigo-500/70 dark:ring-offset-slate-900";break;case"tertiary":u="bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-300 border dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 ring-transparent focus:ring-indigo-400/50 focus:border-indigo-200 dark:ring-offset-slate-900";break;case"warning":u="bg-red-50 dark:bg-red-500/10 text-red-600 dark:text-red-300 border-red-100 dark:border-red-600/50 border hover:text-red-700 hover:bg-red-100 dark:hover:bg-red-500/20 dark:hover:text-red-200 ring-transparent focus:ring-red-500 focus:border-red-500 dark:ring-offset-slate-900";break}const s=i.type==="button"||i.type==="submit";let a="";switch(e){case"small":a="text-base px-4 py-1.5 font-semibold";break;case"medium":a="text-base px-5 py-3 sm:py-2.5";break;default:a="text-lg px-10 py-2.5";break}const h=J(u,"ring ring-offset-0 focus:ring-offset-2 rounded-xl font-bold transition duration-200 outline-none block active:scale-[0.98] leading-[1.25em] select-none",a,t?"w-full":"w-auto",!s&&"text-center",l);return s?S("button",{type:i.type,className:h,disabled:o,...n?{"data-test":n}:{},...i.type==="button"?{onClick:o?()=>{}:()=>i.onClick()}:{},children:i.children}):i.type==="external"?S("a",{className:h,...n?{"data-test":n}:{},...o?{onClick:d=>d.preventDefault()}:{href:i.href},children:i.children}):S(r0,{className:h,to:o?"#":i.to,...n?{"data-test":n}:{},onClick:o?d=>d.preventDefault():()=>{},children:i.children})},o0=({children:e,htmlFor:t,className:n})=>S("label",{htmlFor:t,className:J("text-left text-slate-700 dark:text-slate-200 font-medium text-md mb-1",n),children:e});function i0(e){return u0(new Date(e),"dateInput")}function u0(e,t){return t==="short"?e.toLocaleDateString():t==="url"?[`${e.getMonth()+1}`.padStart(2,"0"),`${e.getDate()}`.padStart(2,"0"),`${e.getFullYear()}`].join("-"):t==="dateInput"?[`${e.getFullYear()}`,`${e.getMonth()+1}`.padStart(2,"0"),`${e.getDate()}`.padStart(2,"0")].join("-"):[e.toLocaleDateString("en-US",{weekday:"long"}),", ",e.toLocaleDateString("en-US",{month:t==="long"?"long":"short"})," ",e.getDate(),", ",e.getFullYear()].join("")}const s0=({label:e,optional:t,value:n,setValue:r,required:l=!1,autoFocus:o=!1,placeholder:i,className:u,disabled:s,name:a,testId:h,...d})=>{const[m,y]=E.useState(n),w=E.useId(),k=xt(d)?"input":"textarea";return O("div",{className:J("flex flex-col space-y-1 w-full",u),children:[(e||t)&&O("div",{className:"flex flex-row justify-between items-center",children:[e&&S(o0,{htmlFor:w,children:e}),t&&S("span",{className:"text-violet-500/80 font-medium translate-y-px text-sm antialiased italic",children:"*optional"})]}),O("div",{className:"flex shadow-sm rounded-xl",children:[xt(d)&&d.prefix&&S("div",{className:"hidden xs:flex justify-center items-center p-3 bg-slate-50 dark:bg-slate-700/50 border border-r-0 dark:border-slate-700 rounded-l-xl",children:S("h3",{className:"text-slate-500 dark:text-slate-400",children:d.prefix})}),S(k,{id:w,type:d.type==="positiveInteger"?"number":d.type,value:m,required:!!l,autoFocus:o,placeholder:i,disabled:s,name:a,...h?{"data-test":h}:{},...d.type==="url"?{autoCapitalize:"none",autoCorrect:"off"}:{},...d.type==="password"?{minLength:4}:{},...d.type==="date"?{min:i0(new Date().toISOString())}:{},...xt(d)?{}:{rows:d.rows},onChange:F=>{const f=F.target.value;y(f),(d.type!=="positiveInteger"||a0(f))&&r(f)},className:J("py-3 px-4 flex-grow w-12","border border-slate-200 rounded-xl","transition-[border-color,ring-color] duration-150","text-slate-600 placeholder:text-slate-400/90 placeholder:antialiased","ring-0 ring-slate-200 outline-none focus:shadow-md focus:border-indigo-500 focus:ring-indigo-500 focus:ring-1","dark:bg-slate-700/20 dark:border-slate-700 dark:placeholder:text-slate-500 dark:text-white",!xt(d)&&d.noResize&&"resize-none",xt(d)&&d.unit&&"rounded-r-none",xt(d)&&d.prefix&&"xs:rounded-l-none")}),xt(d)&&d.unit&&S("div",{className:"flex justify-center items-center p-3 bg-slate-50 dark:bg-slate-700/50 border border-l-0 dark:border-slate-700 rounded-r-xl",children:S("h3",{className:"text-slate-500 dark:text-slate-400",children:d.unit})})]})]})};function a0(e){return e.match(/^[0-9]+$/)!==null&&Number.isInteger(Number(e))&&Number(e)>=0}function xt(e){return e.type!=="textarea"}var c0=Object.defineProperty,f0=(e,t,n)=>t in e?c0(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,uo=(e,t,n)=>(f0(e,typeof t!="symbol"?t+"":t,n),n);let d0=class{constructor(){uo(this,"current",this.detect()),uo(this,"handoffState","pending"),uo(this,"currentId",0)}set(t){this.current!==t&&(this.handoffState="pending",this.currentId=0,this.current=t)}reset(){this.set(this.detect())}nextId(){return++this.currentId}get isServer(){return this.current==="server"}get isClient(){return this.current==="client"}detect(){return typeof window>"u"||typeof document>"u"?"server":"client"}handoff(){this.handoffState==="pending"&&(this.handoffState="complete")}get isHandoffComplete(){return this.handoffState==="complete"}},rn=new d0,pn=(e,t)=>{rn.isServer?E.useEffect(e,t):E.useLayoutEffect(e,t)};function p0(e){let t=E.useRef(e);return pn(()=>{t.current=e},[e]),t}function h0(e){typeof queueMicrotask=="function"?queueMicrotask(e):Promise.resolve().then(e).catch(t=>setTimeout(()=>{throw t}))}function m0(){let e=[],t=[],n={enqueue(r){t.push(r)},addEventListener(r,l,o,i){return r.addEventListener(l,o,i),n.add(()=>r.removeEventListener(l,o,i))},requestAnimationFrame(...r){let l=requestAnimationFrame(...r);return n.add(()=>cancelAnimationFrame(l))},nextFrame(...r){return n.requestAnimationFrame(()=>n.requestAnimationFrame(...r))},setTimeout(...r){let l=setTimeout(...r);return n.add(()=>clearTimeout(l))},microTask(...r){let l={current:!0};return h0(()=>{l.current&&r[0]()}),n.add(()=>{l.current=!1})},add(r){return e.push(r),()=>{let l=e.indexOf(r);if(l>=0){let[o]=e.splice(l,1);o()}}},dispose(){for(let r of e.splice(0))r()},async workQueue(){for(let r of t.splice(0))await r()},style(r,l,o){let i=r.style.getPropertyValue(l);return Object.assign(r.style,{[l]:o}),this.add(()=>{Object.assign(r.style,{[l]:i})})}};return n}function v0(){let[e]=E.useState(m0);return E.useEffect(()=>()=>e.dispose(),[e]),e}let it=function(e){let t=p0(e);return _e.useCallback((...n)=>t.current(...n),[t])};function g0(){let[e,t]=E.useState(rn.isHandoffComplete);return e&&rn.isHandoffComplete===!1&&t(!1),E.useEffect(()=>{e!==!0&&t(!0)},[e]),E.useEffect(()=>rn.handoff(),[]),e}var Ts;let uu=(Ts=_e.useId)!=null?Ts:function(){let e=g0(),[t,n]=_e.useState(e?()=>rn.nextId():null);return pn(()=>{t===null&&n(rn.nextId())},[t]),t!=null?""+t:void 0};function Yc(e,t,...n){if(e in t){let l=t[e];return typeof l=="function"?l(...n):l}let r=new Error(`Tried to handle "${e}" but there is no handler defined. Only defined handlers are: ${Object.keys(t).map(l=>`"${l}"`).join(", ")}.`);throw Error.captureStackTrace&&Error.captureStackTrace(r,Yc),r}function Ls(e){var t;if(e.type)return e.type;let n=(t=e.as)!=null?t:"button";if(typeof n=="string"&&n.toLowerCase()==="button")return"button"}function y0(e,t){let[n,r]=E.useState(()=>Ls(e));return pn(()=>{r(Ls(e))},[e.type,e.as]),pn(()=>{n||!t.current||t.current instanceof HTMLButtonElement&&!t.current.hasAttribute("type")&&r("button")},[n,t]),n}let w0=Symbol();function su(...e){let t=E.useRef(e);E.useEffect(()=>{t.current=e},[e]);let n=it(r=>{for(let l of t.current)l!=null&&(typeof l=="function"?l(r):l.current=r)});return e.every(r=>r==null||r?.[w0])?void 0:n}function k0(...e){return e.filter(Boolean).join(" ")}var S0=(e=>(e[e.None=0]="None",e[e.RenderStrategy=1]="RenderStrategy",e[e.Static=2]="Static",e))(S0||{}),x0=(e=>(e[e.Unmount=0]="Unmount",e[e.Hidden=1]="Hidden",e))(x0||{});function ar({ourProps:e,theirProps:t,slot:n,defaultTag:r,features:l,visible:o=!0,name:i}){let u=Gc(t,e);if(o)return Lr(u,n,r,i);let s=l??0;if(s&2){let{static:a=!1,...h}=u;if(a)return Lr(h,n,r,i)}if(s&1){let{unmount:a=!0,...h}=u;return Yc(a?0:1,{[0](){return null},[1](){return Lr({...h,hidden:!0,style:{display:"none"}},n,r,i)}})}return Lr(u,n,r,i)}function Lr(e,t={},n,r){var l;let{as:o=n,children:i,refName:u="ref",...s}=so(e,["unmount","static"]),a=e.ref!==void 0?{[u]:e.ref}:{},h=typeof i=="function"?i(t):i;s.className&&typeof s.className=="function"&&(s.className=s.className(t));let d={};if(t){let m=!1,y=[];for(let[w,k]of Object.entries(t))typeof k=="boolean"&&(m=!0),k===!0&&y.push(w);m&&(d["data-headlessui-state"]=y.join(" "))}if(o===E.Fragment&&Object.keys(ai(s)).length>0){if(!E.isValidElement(h)||Array.isArray(h)&&h.length>1)throw new Error(['Passing props on "Fragment"!',"",`The current component <${r} /> is rendering a "Fragment".`,"However we need to passthrough the following props:",Object.keys(s).map(w=>` - ${w}`).join(` `),"","You can apply a few solutions:",['Add an `as="..."` prop, to ensure that we render an actual element instead of a "Fragment".',"Render a single element as the child so that we can forward the props onto that element."].map(w=>` - ${w}`).join(` `)].join(` -`));let m=k0((l=h.props)==null?void 0:l.className,s.className),y=m?{className:m}:{};return E.cloneElement(h,Object.assign({},Gc(h.props,ai(so(s,["ref"]))),d,a,E0(h.ref,a.ref),y))}return E.createElement(o,Object.assign({},so(s,["ref"]),o!==E.Fragment&&a,o!==E.Fragment&&d),h)}function E0(...e){return{ref:e.every(t=>t==null)?void 0:t=>{for(let n of e)n!=null&&(typeof n=="function"?n(t):n.current=t)}}}function Gc(...e){if(e.length===0)return{};if(e.length===1)return e[0];let t={},n={};for(let r of e)for(let l in r)l.startsWith("on")&&typeof r[l]=="function"?(n[l]!=null||(n[l]=[]),n[l].push(r[l])):t[l]=r[l];if(t.disabled||t["aria-disabled"])return Object.assign(t,Object.fromEntries(Object.keys(n).map(r=>[r,void 0])));for(let r in n)Object.assign(t,{[r](l,...o){let i=n[r];for(let u of i){if((l instanceof Event||l?.nativeEvent instanceof Event)&&l.defaultPrevented)return;u(l,...o)}}});return t}function Ll(e){var t;return Object.assign(E.forwardRef(e),{displayName:(t=e.displayName)!=null?t:e.name})}function ai(e){let t=Object.assign({},e);for(let n in t)t[n]===void 0&&delete t[n];return t}function so(e,t=[]){let n=Object.assign({},e);for(let r of t)r in n&&delete n[r];return n}function C0(e){let t=e.parentElement,n=null;for(;t&&!(t instanceof HTMLFieldSetElement);)t instanceof HTMLLegendElement&&(n=t),t=t.parentElement;let r=t?.getAttribute("disabled")==="";return r&&N0(n)?!1:r}function N0(e){if(!e)return!1;let t=e.previousElementSibling;for(;t!==null;){if(t instanceof HTMLLegendElement)return!1;t=t.previousElementSibling}return!0}function _0(e){var t;let n=(t=e?.form)!=null?t:e.closest("form");if(n){for(let r of n.elements)if(r.tagName==="INPUT"&&r.type==="submit"||r.tagName==="BUTTON"&&r.type==="submit"||r.nodeName==="INPUT"&&r.type==="image"){r.click();return}}}let P0="div";var Xc=(e=>(e[e.None=1]="None",e[e.Focusable=2]="Focusable",e[e.Hidden=4]="Hidden",e))(Xc||{});let T0=Ll(function(e,t){let{features:n=1,...r}=e,l={ref:t,"aria-hidden":(n&2)===2?!0:void 0,style:{position:"fixed",top:1,left:1,width:1,height:0,padding:0,margin:-1,overflow:"hidden",clip:"rect(0, 0, 0, 0)",whiteSpace:"nowrap",borderWidth:"0",...(n&4)===4&&(n&2)!==2&&{display:"none"}}};return ar({ourProps:l,theirProps:r,slot:{},defaultTag:P0,name:"Hidden"})});var ci=(e=>(e.Space=" ",e.Enter="Enter",e.Escape="Escape",e.Backspace="Backspace",e.Delete="Delete",e.ArrowLeft="ArrowLeft",e.ArrowUp="ArrowUp",e.ArrowRight="ArrowRight",e.ArrowDown="ArrowDown",e.Home="Home",e.End="End",e.PageUp="PageUp",e.PageDown="PageDown",e.Tab="Tab",e))(ci||{});function L0(e,t,n){let[r,l]=E.useState(n),o=e!==void 0,i=E.useRef(o),u=E.useRef(!1),s=E.useRef(!1);return o&&!i.current&&!u.current?(u.current=!0,i.current=o,console.error("A component is changing from uncontrolled to controlled. This may be caused by the value changing from undefined to a defined value, which should not happen.")):!o&&i.current&&!s.current&&(s.current=!0,i.current=o,console.error("A component is changing from controlled to uncontrolled. This may be caused by the value changing from a defined value to undefined, which should not happen.")),[o?e:r,it(a=>(o||l(a),t?.(a)))]}let Zc=E.createContext(null);function Jc(){let e=E.useContext(Zc);if(e===null){let t=new Error("You used a component, but it is not inside a relevant parent.");throw Error.captureStackTrace&&Error.captureStackTrace(t,Jc),t}return e}function z0(){let[e,t]=E.useState([]);return[e.length>0?e.join(" "):void 0,E.useMemo(()=>function(n){let r=it(o=>(t(i=>[...i,o]),()=>t(i=>{let u=i.slice(),s=u.indexOf(o);return s!==-1&&u.splice(s,1),u}))),l=E.useMemo(()=>({register:r,slot:n.slot,name:n.name,props:n.props}),[r,n.slot,n.name,n.props]);return _e.createElement(Zc.Provider,{value:l},n.children)},[t])]}let O0="p",R0=Ll(function(e,t){let n=uu(),{id:r=`headlessui-description-${n}`,...l}=e,o=Jc(),i=su(t);pn(()=>o.register(r),[r,o.register]);let u={ref:i,...o.props,id:r};return ar({ourProps:u,theirProps:l,slot:o.slot||{},defaultTag:O0,name:o.name||"Description"})}),qc=E.createContext(null);function bc(){let e=E.useContext(qc);if(e===null){let t=new Error("You used a