diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index 7352f42..d5623ae 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -6,15 +6,19 @@ on: paths: - 'AllHandsOnDeck/**' - 'AllHandsOnDeckTests/**' + - 'AllHandsOnDeckUITests/**' - 'AllHandsOnDeckWatch/**' - 'project.yml' + - 'scripts/*ci_xcconfig.py' - '.github/workflows/ios-ci.yml' pull_request: paths: - 'AllHandsOnDeck/**' - 'AllHandsOnDeckTests/**' + - 'AllHandsOnDeckUITests/**' - 'AllHandsOnDeckWatch/**' - 'project.yml' + - 'scripts/*ci_xcconfig.py' - '.github/workflows/ios-ci.yml' permissions: @@ -62,14 +66,8 @@ jobs: LIVEKIT_TOKEN_ENDPOINT: ${{ vars.VITE_LIVEKIT_TOKEN_ENDPOINT }} LIVEKIT_BETA_ENABLED: ${{ vars.VITE_ENABLE_LIVEKIT_BETA }} run: | - cat > Secrets.xcconfig < String { let alphabet = Array("ABCDEFGHJKLMNPQRSTUVWXYZ23456789") // omits ambiguous chars - return String((0..<8).map { _ in alphabet.randomElement()! }) + return String((0..<8).map { _ in alphabet[Int.random(in: alphabet.indices)] }) } /// Override the join base URL via UserDefaults["joinBaseURL"] or the diff --git a/AllHandsOnDeck/Core/Services/Identity/IdentityService.swift b/AllHandsOnDeck/Core/Services/Identity/IdentityService.swift index 8cba4df..85a07d6 100644 --- a/AllHandsOnDeck/Core/Services/Identity/IdentityService.swift +++ b/AllHandsOnDeck/Core/Services/Identity/IdentityService.swift @@ -28,7 +28,7 @@ final class IdentityService: ObservableObject { /// The rank earned based on accumulated action points. @Published private(set) var earnedRank: PirateRank = .cabinBoy - private var actionPoints: Int { + private(set) var actionPoints: Int { get { UserDefaults.standard.integer(forKey: "identity.actionPoints") } set { UserDefaults.standard.set(newValue, forKey: "identity.actionPoints") diff --git a/AllHandsOnDeck/Core/Services/Session/MockSessionTransport.swift b/AllHandsOnDeck/Core/Services/Session/MockSessionTransport.swift index d173312..9dd5080 100644 --- a/AllHandsOnDeck/Core/Services/Session/MockSessionTransport.swift +++ b/AllHandsOnDeck/Core/Services/Session/MockSessionTransport.swift @@ -27,7 +27,7 @@ final class MockSessionTransport: SessionTransport { statusSubject.send(.connecting) try? await Task.sleep(nanoseconds: 100_000_000) statusSubject.send(.connected) - + if role == .viewer { // Simulate host sending initial metadata and joining let mockSession = PhotoSession( diff --git a/AllHandsOnDeck/Core/Services/Session/SupabaseRealtimeFrameChannel.swift b/AllHandsOnDeck/Core/Services/Session/SupabaseRealtimeFrameChannel.swift index 38cf086..77876f5 100644 --- a/AllHandsOnDeck/Core/Services/Session/SupabaseRealtimeFrameChannel.swift +++ b/AllHandsOnDeck/Core/Services/Session/SupabaseRealtimeFrameChannel.swift @@ -73,10 +73,11 @@ enum SupabaseFrameBroadcast { let event: String let payload: BroadcastPayload? - struct BroadcastPayload: Decodable { - let event: String? - let payload: FramePayload? - } + } + + private struct BroadcastPayload: Decodable { + let event: String? + let payload: FramePayload? } /// Decodes a raw websocket message into a frame, or `nil` if the message diff --git a/AllHandsOnDeck/Features/Home/HomeView.swift b/AllHandsOnDeck/Features/Home/HomeView.swift index 9e41c3a..f76194d 100644 --- a/AllHandsOnDeck/Features/Home/HomeView.swift +++ b/AllHandsOnDeck/Features/Home/HomeView.swift @@ -146,7 +146,7 @@ struct HomeView: View { } } } - + Button { withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { activeTab = .host @@ -328,6 +328,7 @@ struct HomeView: View { // MARK: - Ambient Glow struct AmbientGlowView: View { + @Environment(\.accessibilityReduceMotion) private var reduceMotion @State private var animate = false var body: some View { @@ -343,7 +344,7 @@ struct AmbientGlowView: View { .frame(width: 300, height: 300) .blur(radius: 50) .offset(x: animate ? -40 : -80, y: animate ? -100 : -150) - + // Orb 2 Circle() .fill(RadialGradient( @@ -357,11 +358,14 @@ struct AmbientGlowView: View { .offset(x: animate ? 80 : 40, y: animate ? 150 : 200) } .onAppear { + guard !reduceMotion, + !ProcessInfo.processInfo.arguments.contains("-disableAnimations") else { return } withAnimation(.easeInOut(duration: 8).repeatForever(autoreverses: true)) { animate.toggle() } } .allowsHitTesting(false) + .accessibilityHidden(true) } } diff --git a/AllHandsOnDeck/Features/Settings/IdentitySettingsView.swift b/AllHandsOnDeck/Features/Settings/IdentitySettingsView.swift index b488deb..4d0a862 100644 --- a/AllHandsOnDeck/Features/Settings/IdentitySettingsView.swift +++ b/AllHandsOnDeck/Features/Settings/IdentitySettingsView.swift @@ -11,21 +11,22 @@ struct IdentitySettingsView: View { ZStack { LeopardWallpaperView() ScrollView { - VStack(spacing: 20) { + VStack(spacing: Spacing.xl) { rankCard customNameSection gameCenterSection progressSection } - .padding(20) + .padding(Spacing.xl) } } - .navigationTitle(String(localized: "identity.settings.title")) + .navigationTitle(DesignLabels.identitySettingsTitle) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .confirmationAction) { - Button(String(localized: "OK")) { dismiss() } + Button(DesignLabels.done) { dismiss() } .foregroundStyle(Theme.gold) + .accessibilityIdentifier("identity_done") } } } @@ -45,7 +46,7 @@ struct IdentitySettingsView: View { .font(.system(size: 13, weight: .medium, design: .rounded)) .foregroundStyle(Theme.mist) .multilineTextAlignment(.center) - .padding(.horizontal, 16) + .padding(.horizontal, Spacing.lg) } .frame(maxWidth: .infinity) .padding(.vertical, 28) @@ -54,21 +55,25 @@ struct IdentitySettingsView: View { } private var customNameSection: some View { - VStack(alignment: .leading, spacing: 10) { - label("identity.customName.label", image: "person.fill") - - TextField(String(localized: "identity.customName.placeholder"), text: $identity.customName) + VStack(alignment: .leading, spacing: Spacing.md) { + label(DesignLabels.identityCustomNameLabel, image: "person.fill") + + TextField(DesignLabels.identityCustomNamePlaceholder, text: $identity.customName) + .accessibilityIdentifier("identity_custom_name") + .accessibilityLabel(DesignLabels.identityCustomNameLabel) + .submitLabel(.done) + .onSubmit { nameFieldFocused = false } .textFieldStyle(.plain) .foregroundStyle(Theme.bone) .tint(Theme.gold) .focused($nameFieldFocused) - .padding(.horizontal, 16) - .frame(height: 48) + .padding(.horizontal, Spacing.lg) + .frame(height: Spacing.buttonHeight) .background(Color.white.opacity(0.08)) - .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + .clipShape(RoundedRectangle(cornerRadius: Spacing.cornerMd, style: .continuous)) if !identity.customName.trimmingCharacters(in: .whitespaces).isEmpty { - Text(String(localized: "identity.customName.hint")) + Text(DesignLabels.identityCustomNameHint) .font(.caption) .foregroundStyle(Theme.mist) } @@ -76,8 +81,8 @@ struct IdentitySettingsView: View { } private var gameCenterSection: some View { - VStack(alignment: .leading, spacing: 10) { - label("identity.gamecenter.label", image: "gamecontroller.fill") + VStack(alignment: .leading, spacing: Spacing.md) { + label(DesignLabels.identityGameCenterLabel, image: "gamecontroller.fill") HStack { VStack(alignment: .leading, spacing: 3) { @@ -85,17 +90,17 @@ struct IdentitySettingsView: View { Text(alias) .font(.system(size: 15, weight: .heavy, design: .rounded)) .foregroundStyle(Theme.bone) - Text(String(localized: "identity.gamecenter.connected")) + Text(DesignLabels.identityGameCenterConnected) .font(.caption) .foregroundStyle(Theme.signal) } else { - Text(String(localized: "identity.gamecenter.disconnected")) + Text(DesignLabels.identityGameCenterDisconnected) .font(.system(size: 14, weight: .medium, design: .rounded)) .foregroundStyle(Theme.mist) } } Spacer() - Toggle("", isOn: Binding( + Toggle(DesignLabels.identityGameCenterLabel, isOn: Binding( get: { identity.useGameCenter }, set: { on in if on { @@ -106,15 +111,16 @@ struct IdentitySettingsView: View { } )) .labelsHidden() + .accessibilityIdentifier("identity_game_center") .tint(Theme.gold) } - .padding(.horizontal, 16) + .padding(.horizontal, Spacing.lg) .padding(.vertical, 12) .background(Color.white.opacity(0.08)) - .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + .clipShape(RoundedRectangle(cornerRadius: Spacing.cornerMd, style: .continuous)) if identity.useGameCenter && !gc.isAuthenticated { - Text(String(localized: "identity.gamecenter.notSignedIn")) + Text(DesignLabels.identityGameCenterNotSignedIn) .font(.caption) .foregroundStyle(Theme.amber) } @@ -122,10 +128,10 @@ struct IdentitySettingsView: View { } private var progressSection: some View { - VStack(alignment: .leading, spacing: 10) { - label("identity.progress.label", image: "chart.bar.fill") + VStack(alignment: .leading, spacing: Spacing.md) { + label(DesignLabels.identityProgressLabel, image: "chart.bar.fill") - let points = UserDefaults.standard.integer(forKey: "identity.actionPoints") + let points = identity.actionPoints let nextRank = PirateRank(rawValue: identity.earnedRank.rawValue + 1) let nextThreshold = nextRank?.threshold ?? Int.max let current = identity.earnedRank @@ -135,7 +141,7 @@ struct IdentitySettingsView: View { Text("\(points)") .font(.system(size: 28, weight: .black, design: .rounded)) .foregroundStyle(Theme.gold) - Text(String(localized: "identity.progress.points")) + Text(DesignLabels.identityProgressPoints) .font(.system(size: 14, weight: .medium)) .foregroundStyle(Theme.mist) .padding(.top, 6) @@ -158,25 +164,26 @@ struct IdentitySettingsView: View { .tint(Theme.gold) } } else { - Text(String(localized: "identity.progress.maxRank")) + Text(DesignLabels.identityProgressMaxRank) .font(.caption) .foregroundStyle(Theme.signal) } } - .padding(16) + .padding(Spacing.lg) .background(Color.white.opacity(0.08)) - .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) + .clipShape(RoundedRectangle(cornerRadius: Spacing.cornerMd, style: .continuous)) - Text(String(localized: "identity.progress.hint")) + Text(DesignLabels.identityProgressHint) .font(.caption) .foregroundStyle(Theme.mist) } } - private func label(_ key: String, image: String) -> some View { - Label(String(localized: String.LocalizationValue(key)), - systemImage: image) + private func label(_ title: String, image: String) -> some View { + Label(title, systemImage: image) .font(.system(size: 13, weight: .heavy, design: .rounded)) .foregroundStyle(Theme.mist) } } + +#Preview { IdentitySettingsView() } diff --git a/AllHandsOnDeck/Shared/Utilities/DesignLabels.swift b/AllHandsOnDeck/Shared/Utilities/DesignLabels.swift index 8dac35f..c49ef11 100644 --- a/AllHandsOnDeck/Shared/Utilities/DesignLabels.swift +++ b/AllHandsOnDeck/Shared/Utilities/DesignLabels.swift @@ -4,6 +4,20 @@ import SwiftUI /// All 3 surfaces (host, viewer, webapp) reference these for consistency. /// English is the default language; no localization fallback. enum DesignLabels { + // MARK: - Identity settings + static let identitySettingsTitle = String(localized: "identity.settings.title") + static let identityCustomNameLabel = String(localized: "identity.customName.label") + static let identityCustomNamePlaceholder = String(localized: "identity.customName.placeholder") + static let identityCustomNameHint = String(localized: "identity.customName.hint") + static let identityGameCenterLabel = String(localized: "identity.gamecenter.label") + static let identityGameCenterConnected = String(localized: "identity.gamecenter.connected") + static let identityGameCenterDisconnected = String(localized: "identity.gamecenter.disconnected") + static let identityGameCenterNotSignedIn = String(localized: "identity.gamecenter.notSignedIn") + static let identityProgressLabel = String(localized: "identity.progress.label") + static let identityProgressPoints = String(localized: "identity.progress.points") + static let identityProgressMaxRank = String(localized: "identity.progress.maxRank") + static let identityProgressHint = String(localized: "identity.progress.hint") + // MARK: - Buttons static let cancel = "Cancel" static let close = "Close" diff --git a/AllHandsOnDeckUITests/HappyPathUITests.swift b/AllHandsOnDeckUITests/HappyPathUITests.swift index 0cc464f..60abf54 100644 --- a/AllHandsOnDeckUITests/HappyPathUITests.swift +++ b/AllHandsOnDeckUITests/HappyPathUITests.swift @@ -17,7 +17,8 @@ final class HappyPathUITests: XCTestCase { app.launchArguments = [ "-useMockTransport", "YES", "-allowWebJoinDefault", "YES", - "-bypassCameraPermission" + "-bypassCameraPermission", + "-disableAnimations" ] app.launch() } @@ -26,6 +27,22 @@ final class HappyPathUITests: XCTestCase { app.terminate() } + func test_identitySettingsControlsAreAccessible() throws { + let settings = app.buttons["Identity settings"] + XCTAssertTrue(settings.waitForExistence(timeout: 5)) + settings.tap() + XCTAssertTrue(app.textFields["identity_custom_name"].waitForExistence(timeout: 5)) + let gameCenter = app.switches["identity_game_center"] + XCTAssertTrue(gameCenter.exists) + XCTAssertFalse(gameCenter.label.isEmpty) + let screenshot = XCTAttachment(screenshot: app.screenshot()) + screenshot.name = "Identity settings" + screenshot.lifetime = .keepAlways + add(screenshot) + app.buttons["identity_done"].tap() + XCTAssertTrue(settings.waitForExistence(timeout: 5)) + } + // MARK: - Host: Session Lifecycle ────────────────────────────────────────── func test_startHostSession_reachesCameraView() throws { @@ -242,7 +259,7 @@ final class HappyPathUITests: XCTestCase { // Initially on Join Crew tab XCTAssertTrue(app.buttons["Join Session"].waitForExistence(timeout: 5)) XCTAssertTrue(app.buttons["Nearby Sessions"].exists) - + // Switch to Captain tab let captainTab = app.buttons["Captain"] if captainTab.waitForExistence(timeout: 5) { diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ba74a2..002bcf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Fixed — 2026-09-15 App audit (2.4.4) +- Pasted web invitation URLs now preserve the session code and case-sensitive token; malformed and overlong codes are rejected. +- Web preview quality and resolution settings now apply to transmitted frames. +- Timer cancellation now prevents capture, including after navigation. Browser-hosted sessions accurately advertise captain-only capture; removed ineffective permission buttons. +- Host navigation releases the camera, including delayed permission results and startup failures. +- Landing page legal links resolve to existing documents; added loading feedback, keyboard focus, reduced-motion support, and English page language. +- Removed the ineffective nearby-only toggle from browser hosting and clarified web access. +- iOS identity settings use centralized labels, accessible controls, keyboard dismissal, and service-owned persistence reads. +- Refreshed web/server dependency manifests and lockfiles, including validated Vitest 5 migration; both npm audits report zero vulnerabilities. +- Aligned iOS marketing version with the web version. The old DebugOverlayView.swift no longer exists in this repository. + + ### Changed — 2026-06-12 Streaming & Performance - **Preview frames now travel over Supabase Realtime Broadcast** (topic `session-frames:{sessions.id}`, event `preview_frame`) instead of `session_events` INSERTs — no more database churn from ~3 fps frame traffic (was 42 MB / 95% of all rows after ~100 test sessions) - iOS host sends frames via REST `POST /realtime/v1/api/broadcast` (fire-and-forget); iOS viewers receive through a new minimal Phoenix-websocket client (`SupabaseRealtimeFrameChannel.swift`) with heartbeat + reconnect diff --git a/docs/evidence/ahod-122/README.md b/docs/evidence/ahod-122/README.md new file mode 100644 index 0000000..83ebe39 --- /dev/null +++ b/docs/evidence/ahod-122/README.md @@ -0,0 +1,36 @@ +# App audit — GitHub #122 + +## Changes + +- Landing: parse full invitations without truncation or token case changes, reject malformed codes, fix legal links, centralize copy, expose loading/focus/pressed states, honor reduced motion. +- Web hosting: apply preview size/quality controls, stop camera tracks on navigation and startup failure, discard late session creation, make countdown cancellation prevent capture. +- Browser capture policy: advertise `hostOnly` and explain captain-controlled capture. The previous permission buttons did not implement viewer-triggered capture; they have been removed. iOS capture permissions are unchanged. +- iOS identity settings: centralized localized labels, named accessible controls, Done keyboard action, service-owned persistence read, spacing tokens. +- Dependencies: updated manifests and lockfiles in webapp/server, including validated Vitest 5.0.1 migration; zero npm audit findings. Already-merged upstream TypeScript/jsdom major upgrades retained. No external Swift packages are configured in project.yml. +- Version: 2.4.4 on web home/join and iOS marketing version. DebugOverlayView.swift no longer exists. +- SwiftLint: fixed six existing findings and moved the analyzer-only rule to analyzer_rules. + +## Validation + +- Web: 59 unit tests; production build; npm audit. +- Browser: mobile 320/390px and desktop 1440px layouts, reduced motion, invite input, legal links, host/join flows; 25 passed, one environment-specific no-backend fallback skipped when backend is configured. +- Server: 10 tests, build, npm audit. +- iOS: 83 unit tests; three UI tests (identity accessibility, host settings sheet, back navigation) on iPhone 17 Pro Max / iOS 26.5. Unit suite also passed on iOS 27 before final lint cleanup. +- SwiftLint strict: zero violations. +- Independent code review: no critical/important findings after lifecycle fixes. + +## Screenshots + +![Mobile landing](home-mobile.png) +![Desktop landing](home-desktop.png) +![iOS identity settings](ios-settings.png) + +## Limits + +Physical multi-device camera/Watch testing and App Store publishing are not covered by this audit. Browser-hosted viewer-triggered capture remains unsupported and is now presented accurately. Preview bandwidth varies with the selected quality and frame size; no production latency benchmark was performed. + +Linear workspace unavailable; the user explicitly authorized GitHub-only tracking for this work. + +## CI follow-up — 2026-09-16 + +Fixed CI xcconfig URL escaping (raw `//` was parsed as a comment); added four generator tests and deterministic test fixtures for PRs without secrets. CI now includes the identity UI test and UI-test path filters. CodeQL dependency PR #129 merged after review and green checks. diff --git a/docs/evidence/ahod-122/home-desktop.png b/docs/evidence/ahod-122/home-desktop.png new file mode 100644 index 0000000..0819f40 Binary files /dev/null and b/docs/evidence/ahod-122/home-desktop.png differ diff --git a/docs/evidence/ahod-122/home-mobile.png b/docs/evidence/ahod-122/home-mobile.png new file mode 100644 index 0000000..1833bd2 Binary files /dev/null and b/docs/evidence/ahod-122/home-mobile.png differ diff --git a/docs/evidence/ahod-122/ios-settings.png b/docs/evidence/ahod-122/ios-settings.png new file mode 100644 index 0000000..598a4fb Binary files /dev/null and b/docs/evidence/ahod-122/ios-settings.png differ diff --git a/project.yml b/project.yml index efb2a0a..abaa5dc 100644 --- a/project.yml +++ b/project.yml @@ -18,7 +18,7 @@ options: settings: base: SWIFT_VERSION: "5.0" - MARKETING_VERSION: "1.0.0" + MARKETING_VERSION: "2.4.4" CURRENT_PROJECT_VERSION: "1" DEVELOPMENT_TEAM: LPHP8KBWW8 CODE_SIGN_STYLE: Automatic diff --git a/scripts/test_write_ci_xcconfig.py b/scripts/test_write_ci_xcconfig.py new file mode 100644 index 0000000..09cc08c --- /dev/null +++ b/scripts/test_write_ci_xcconfig.py @@ -0,0 +1,33 @@ +import tempfile +import unittest +from pathlib import Path +from write_ci_xcconfig import encode_value, write_config + + +class CIConfigTests(unittest.TestCase): + def test_urls_are_not_xcconfig_comments(self): + self.assertEqual(encode_value('https://example.com/path'), 'https:/$()/example.com/path') + + def test_rejects_multiline_settings(self): + with self.assertRaises(ValueError): + encode_value('value\nOTHER = value') + + def test_preserves_provided_config(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / 'Secrets.xcconfig' + write_config({'SUPABASE_URL': 'https://example.supabase.co', 'SUPABASE_ANON_KEY': 'a.b.c'}, path) + content = path.read_text() + self.assertIn('SUPABASE_URL = https:/$()/example.supabase.co\n', content) + self.assertIn('SUPABASE_ANON_KEY = a.b.c\n', content) + + def test_absent_secrets_use_test_fixture(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / 'Secrets.xcconfig' + write_config({'SUPABASE_URL': 'https://example.supabase.co'}, path) + content = path.read_text() + self.assertIn('ci-test.supabase.co', content) + self.assertIn('test-only.anon.not-a-real-signature', content) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/write_ci_xcconfig.py b/scripts/write_ci_xcconfig.py new file mode 100644 index 0000000..eb93c0e --- /dev/null +++ b/scripts/write_ci_xcconfig.py @@ -0,0 +1,26 @@ +"""Write CI-only Xcode settings without treating URL slashes as comments.""" +import os +from pathlib import Path + + +def encode_value(value: str) -> str: + if '\n' in value or '\r' in value: + raise ValueError('Xcode settings must be single-line values') + return value.replace('//', '/$()/') + + +def write_config(environment: dict[str, str], output: Path) -> None: + values = {key: environment.get(key, '') for key in ( + 'SUPABASE_URL', 'SUPABASE_ANON_KEY', 'WEB_JOIN_BASE_URL', + 'LIVEKIT_TOKEN_ENDPOINT', 'LIVEKIT_BETA_ENABLED', + )} + # Unit and UI tests use mocks. Fork PRs do not receive repository secrets. + if not values['SUPABASE_URL'] or not values['SUPABASE_ANON_KEY']: + values['SUPABASE_URL'] = 'https://ci-test.supabase.co' + values['SUPABASE_ANON_KEY'] = 'test-only.anon.not-a-real-signature' + values['TEAM_ID'] = 'LPHP8KBWW8' + output.write_text(''.join(f'{key} = {encode_value(value)}\n' for key, value in values.items())) + + +if __name__ == '__main__': + write_config(dict(os.environ), Path('Secrets.xcconfig')) diff --git a/server/package-lock.json b/server/package-lock.json index ec70b23..4289d35 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -8,20 +8,20 @@ "name": "allhands-server", "version": "0.1.0", "dependencies": { - "@supabase/supabase-js": "^2.115.0", + "@supabase/supabase-js": "^2.116.0", "ws": "^8.21.3" }, "devDependencies": { "@types/node": "^26.2.0", "@types/ws": "^8.18.1", - "tsx": "^4.23.12", + "tsx": "^4.23.13", "typescript": "^7.0.2" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], @@ -36,9 +36,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -53,9 +53,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], @@ -70,9 +70,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], @@ -87,9 +87,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], @@ -104,9 +104,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], @@ -121,9 +121,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], @@ -138,9 +138,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], @@ -155,9 +155,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], @@ -172,9 +172,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], @@ -189,9 +189,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], @@ -206,9 +206,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], @@ -223,9 +223,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], @@ -240,9 +240,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], @@ -257,9 +257,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], @@ -274,9 +274,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], @@ -291,9 +291,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], @@ -308,9 +308,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], @@ -325,9 +325,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], @@ -342,9 +342,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], @@ -359,9 +359,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -376,9 +376,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], @@ -393,9 +393,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -410,9 +410,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -427,9 +427,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], @@ -444,9 +444,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -461,9 +461,9 @@ } }, "node_modules/@supabase/auth-js": { - "version": "2.115.0", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.115.0.tgz", - "integrity": "sha512-YNQlQWm1H0gsXHSY8Jd/xepBhjO0Zhwx04iW17A83/joQ5kFiUin6iPj9s9kZZvupnwLjXVP/diTFiLz2jUbwQ==", + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.116.0.tgz", + "integrity": "sha512-Cmosty12gyKGK9N3bQb+lMmuAFev5nmUzaR1AsmZHqKOAGzqX1VQzmp49CNPwOx/pw0H9Qqk4rs9yhwTlKpfDg==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -473,9 +473,9 @@ } }, "node_modules/@supabase/functions-js": { - "version": "2.115.0", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.115.0.tgz", - "integrity": "sha512-p97V6/YFcdp+zblFDVJaE8f9rGKTNz0PRzyJ2d1w/EYIU5lwidKuc3l/wM+u27ACmAC62albvoGiUGzLPSg7Aw==", + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.116.0.tgz", + "integrity": "sha512-E+VOc2QDcni/fySqkBFiZhnoB3SGydEdZgFI6/dEAGAHx6yEhB46TN9qb2wXs+E+RSzOBV0R6dasiSlw4xlZAA==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -491,9 +491,9 @@ "license": "MIT" }, "node_modules/@supabase/postgrest-js": { - "version": "2.115.0", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.115.0.tgz", - "integrity": "sha512-DdERcurLh5t84pgSywDg2LjLR5le7XAzl52/iQhC7FdboWDqGfDPMfaWJV3MHvhyxSlSNzbpSQG+RiQOBSn/4w==", + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.116.0.tgz", + "integrity": "sha512-kGpVZTDHxFTJS3tu+rU0iTAZ+4U0bcLVjxwCk8f3gRhjw3qdCZjTBlgYvc4kGH2XccmAzbkKwXL/mrNHMGSc+A==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -503,9 +503,9 @@ } }, "node_modules/@supabase/realtime-js": { - "version": "2.115.0", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.115.0.tgz", - "integrity": "sha512-5HyBkvlA/IUV2v8jX3uLTdy15jmTPRVXwXKoxvH2SKw5jZMNURxCxt+VpCEukscXQlY7pUpH8Cy4NH6io4iaRw==", + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.116.0.tgz", + "integrity": "sha512-MHAnlXxi2s6yiJsZsQMfs2B3RFxeVfQWxerqYhIMqcCQV/FuY3LIeouPEkXw/ah7wUWMLYwempF9MOCUScyddg==", "license": "MIT", "dependencies": { "@supabase/phoenix": "0.4.5", @@ -516,9 +516,9 @@ } }, "node_modules/@supabase/storage-js": { - "version": "2.115.0", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.115.0.tgz", - "integrity": "sha512-dLyIxzbO+MCcKHhcce8rVUCQX1iyqXqQ8ytgkOVYJ7D+Zp0qKylPtQH3hamgxrGSxtDjaw47Urpzw2iK9PsKdA==", + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.116.0.tgz", + "integrity": "sha512-6/3hR6vccBP6oGM5B6RfbwZcTCKmQOodd/ZWQdsw8yJsU5zO/a//oBL6yLnmgxcjnHSrelW8rsO7hL5DPybyUQ==", "license": "MIT", "dependencies": { "iceberg-js": "^0.8.1", @@ -529,16 +529,16 @@ } }, "node_modules/@supabase/supabase-js": { - "version": "2.115.0", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.115.0.tgz", - "integrity": "sha512-PYJSxtCo37R7tTZW6pAqsxUeSx/dlhA7zn8RzKEUSCqyTxCUhG+iHrDTb04UyVBYbttaUbhwkNTvb8h5HW+uZA==", + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.116.0.tgz", + "integrity": "sha512-YyWmKXt2NspV9iO8FPnlswUFJIRnrLd3oTCb+3ZyYRuKZtBH0xCUDgnUqoyA0fGUxpM/UhfwDjYf/dht/9bp7g==", "license": "MIT", "dependencies": { - "@supabase/auth-js": "2.115.0", - "@supabase/functions-js": "2.115.0", - "@supabase/postgrest-js": "2.115.0", - "@supabase/realtime-js": "2.115.0", - "@supabase/storage-js": "2.115.0" + "@supabase/auth-js": "2.116.0", + "@supabase/functions-js": "2.116.0", + "@supabase/postgrest-js": "2.116.0", + "@supabase/realtime-js": "2.116.0", + "@supabase/storage-js": "2.116.0" }, "engines": { "node": ">=22.0.0" @@ -553,13 +553,13 @@ } }, "node_modules/@types/node": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", - "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "version": "26.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.1.tgz", + "integrity": "sha512-VqGJBMCtdhqkBUCcBLvywI0NJ+KLuVzgNnlBUNFOQjqVxzo2lxLUNg1DSey8+u2u6ktswSAxg+s68QLzWHNOuA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~8.3.0" + "undici-types": "~8.9.0" } }, "node_modules/@types/ws": { @@ -913,9 +913,9 @@ } }, "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -926,32 +926,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/fsevents": { @@ -985,9 +985,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.23.12", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", - "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", "dev": true, "license": "MIT", "dependencies": { @@ -1039,9 +1039,9 @@ } }, "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", "dev": true, "license": "MIT" }, diff --git a/server/package.json b/server/package.json index 5d7b1f8..31c6cfa 100644 --- a/server/package.json +++ b/server/package.json @@ -11,13 +11,13 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@supabase/supabase-js": "^2.115.0", + "@supabase/supabase-js": "^2.116.0", "ws": "^8.21.3" }, "devDependencies": { "@types/node": "^26.2.0", "@types/ws": "^8.18.1", - "tsx": "^4.23.12", + "tsx": "^4.23.13", "typescript": "^7.0.2" } } diff --git a/webapp/e2e/home-regression.spec.ts b/webapp/e2e/home-regression.spec.ts new file mode 100644 index 0000000..0be2ef7 --- /dev/null +++ b/webapp/e2e/home-regression.spec.ts @@ -0,0 +1,46 @@ +import { test, expect } from '@playwright/test'; + +test('pasted invite preserves its session code and case-sensitive token', async ({ page }) => { + await page.goto('/'); + await page.getByPlaceholder('ABCDEF1234').fill('https://remotecameraapp.vercel.app/join/abc123?token=MixedCase-token&expires_at=2026-10-01'); + await page.getByRole('button', { name: 'Join Session' }).click(); + await expect(page).toHaveURL(/\/join\/ABC123\?token=MixedCase-token&expires_at=2026-10-01$/); +}); + +test('invalid session inputs stay disabled instead of being truncated', async ({ page }) => { + await page.goto('/'); + for (const invalid of ['ABCDE', 'ABCDEFGHIJK', 'ABC123!extra', 'https://example.com/other/ABC123']) { + await page.getByPlaceholder('ABCDEF1234').fill(invalid); + await expect(page.getByRole('button', { name: 'Join Session' })).toBeDisabled(); + } +}); + +test('hosting explains web access without offering an ineffective nearby-only switch', async ({ page }) => { + await page.goto('/'); + await page.getByRole('button', { name: 'Captain', exact: true }).click(); + await expect(page.getByText('Crew members can join in a browser using your session code or QR code.')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Start Crew Photo' })).toBeEnabled(); +}); + +for (const width of [320, 390, 1440]) { + test(`home fits ${width}px and honors reduced motion`, async ({ page }, testInfo) => { + await page.setViewportSize({ width, height: 900 }); + await page.emulateMedia({ reducedMotion: 'reduce' }); + await page.goto('/'); + await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width); + await expect(page.locator('.bg-glow-1')).toHaveCSS('animation-name', 'none'); + await page.screenshot({ path: testInfo.outputPath(`home-${width}.png`), fullPage: true }); + await page.getByRole('button', { name: 'Captain', exact: true }).click(); + await page.screenshot({ path: testInfo.outputPath(`host-options-${width}.png`), fullPage: true }); + }); +} + +test('privacy and imprint links open their documents', async ({ page }) => { + for (const label of ['Privacy', 'Imprint']) { + await page.goto('/'); + await page.getByRole('link', { name: label, exact: true }).click(); + await expect(page).toHaveURL(new RegExp(`/${label.toLowerCase()}\\.html$`)); + await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); + } +}); diff --git a/webapp/index.html b/webapp/index.html index d52991a..ab8719f 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -1,5 +1,5 @@ - + diff --git a/webapp/package-lock.json b/webapp/package-lock.json index ed797e4..3c2c872 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -8,22 +8,22 @@ "name": "allhands-webapp", "version": "0.1.0", "dependencies": { - "@supabase/supabase-js": "^2.112.4", + "@supabase/supabase-js": "^2.116.0", "qrcode": "^1.5.4", - "react": "^19.2.7", - "react-dom": "^19.2.8", - "react-router-dom": "^7.18.3" + "react": "^19.3.0", + "react-dom": "^19.3.0", + "react-router-dom": "^7.18.4" }, "devDependencies": { "@playwright/test": "^1.63.0", "@types/qrcode": "^1.5.6", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.7", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", "@vitejs/plugin-react": "^6.1.1", "jsdom": "^30.0.1", "typescript": "^7.0.2", - "vite": "^8.2.2", - "vitest": "^4.1.9" + "vite": "^8.3.0", + "vitest": "^5.0.1" } }, "node_modules/@asamuzakjp/css-color": { @@ -93,9 +93,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", - "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.0.tgz", + "integrity": "sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg==", "dev": true, "funding": [ { @@ -117,9 +117,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz", - "integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.3.tgz", + "integrity": "sha512-y4LpL+lmpuyKDiEFq2PnZUVFdAjsoB/qQJod79yLNokXyW7jewi+/WJ69EfItj8A2unWtxXnGjw6LYXgXu5ZjA==", "dev": true, "funding": [ { @@ -134,7 +134,7 @@ "license": "MIT", "dependencies": { "@csstools/color-helpers": "^6.1.1", - "@csstools/css-calc": "^3.3.0" + "@csstools/css-calc": "^3.4.0" }, "engines": { "node": ">=20.19.0" @@ -230,17 +230,38 @@ } } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "dev": true, "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@oxc-project/types": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.148.0.tgz", - "integrity": "sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==", + "version": "0.149.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", + "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", "dev": true, "license": "MIT", "funding": { @@ -264,9 +285,9 @@ } }, "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.7.tgz", - "integrity": "sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", + "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", "cpu": [ "arm" ], @@ -281,9 +302,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.7.tgz", - "integrity": "sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", + "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", "cpu": [ "arm64" ], @@ -298,9 +319,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.7.tgz", - "integrity": "sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", + "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", "cpu": [ "arm64" ], @@ -315,9 +336,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.7.tgz", - "integrity": "sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", + "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", "cpu": [ "x64" ], @@ -332,9 +353,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.7.tgz", - "integrity": "sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", + "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", "cpu": [ "x64" ], @@ -349,9 +370,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.7.tgz", - "integrity": "sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", + "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", "cpu": [ "arm" ], @@ -366,9 +387,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.7.tgz", - "integrity": "sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", + "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", "cpu": [ "arm64" ], @@ -386,9 +407,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.7.tgz", - "integrity": "sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", + "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", "cpu": [ "arm64" ], @@ -406,9 +427,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.7.tgz", - "integrity": "sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", + "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", "cpu": [ "ppc64" ], @@ -426,9 +447,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.7.tgz", - "integrity": "sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", + "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", "cpu": [ "s390x" ], @@ -446,9 +467,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.7.tgz", - "integrity": "sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", + "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", "cpu": [ "x64" ], @@ -466,9 +487,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.7.tgz", - "integrity": "sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", + "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", "cpu": [ "x64" ], @@ -486,9 +507,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.7.tgz", - "integrity": "sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", + "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", "cpu": [ "arm64" ], @@ -503,9 +524,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.7.tgz", - "integrity": "sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", + "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", "cpu": [ "arm64" ], @@ -520,9 +541,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.7.tgz", - "integrity": "sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", + "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", "cpu": [ "x64" ], @@ -543,17 +564,10 @@ "dev": true, "license": "MIT" }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, "node_modules/@supabase/auth-js": { - "version": "2.112.4", - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.112.4.tgz", - "integrity": "sha512-z8DesgwLzKM5PiT0yNmJU8VJyh1zAhYi+20Z7drdJQLXg/wWW4yGt/un+He5ERYUo94Vz66t5aeyr1DIDemI5A==", + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.116.0.tgz", + "integrity": "sha512-Cmosty12gyKGK9N3bQb+lMmuAFev5nmUzaR1AsmZHqKOAGzqX1VQzmp49CNPwOx/pw0H9Qqk4rs9yhwTlKpfDg==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -563,9 +577,9 @@ } }, "node_modules/@supabase/functions-js": { - "version": "2.112.4", - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.112.4.tgz", - "integrity": "sha512-DQ0aVH8wSQAccVqNoEkec62qCu2QRNyoGN53RqsVZ1k6F1zq4/v8scrlR6LNT2RJmT97apiTmORijPVhErCS2g==", + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.116.0.tgz", + "integrity": "sha512-E+VOc2QDcni/fySqkBFiZhnoB3SGydEdZgFI6/dEAGAHx6yEhB46TN9qb2wXs+E+RSzOBV0R6dasiSlw4xlZAA==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -581,9 +595,9 @@ "license": "MIT" }, "node_modules/@supabase/postgrest-js": { - "version": "2.112.4", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.112.4.tgz", - "integrity": "sha512-uaubtPSeg2TR4wrtfQoQWgkTAe+a0qWX2KhmwvTfNl5mGN9+U7owiJt6abk3o/V6O899PSRD1yzxs5RlF4xTug==", + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.116.0.tgz", + "integrity": "sha512-kGpVZTDHxFTJS3tu+rU0iTAZ+4U0bcLVjxwCk8f3gRhjw3qdCZjTBlgYvc4kGH2XccmAzbkKwXL/mrNHMGSc+A==", "license": "MIT", "dependencies": { "tslib": "2.8.1" @@ -593,9 +607,9 @@ } }, "node_modules/@supabase/realtime-js": { - "version": "2.112.4", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.112.4.tgz", - "integrity": "sha512-vZ+j079SKrM0Xiq7MJCvQKLDpaH2kfKfLY68xuQE1sqsCsMmx1CyrDBJHsxZ3cX01VOs5SI9igmoZAF3BmdZxw==", + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.116.0.tgz", + "integrity": "sha512-MHAnlXxi2s6yiJsZsQMfs2B3RFxeVfQWxerqYhIMqcCQV/FuY3LIeouPEkXw/ah7wUWMLYwempF9MOCUScyddg==", "license": "MIT", "dependencies": { "@supabase/phoenix": "0.4.5", @@ -606,9 +620,9 @@ } }, "node_modules/@supabase/storage-js": { - "version": "2.112.4", - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.112.4.tgz", - "integrity": "sha512-lQ0JemuTlMIXVKgSci1qez8yPnM5hyDngeAfEBjZS2Om4D+Cus0EE5BE6glFobrxdyii1OF4UzWfF0zcQgDq5A==", + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.116.0.tgz", + "integrity": "sha512-6/3hR6vccBP6oGM5B6RfbwZcTCKmQOodd/ZWQdsw8yJsU5zO/a//oBL6yLnmgxcjnHSrelW8rsO7hL5DPybyUQ==", "license": "MIT", "dependencies": { "iceberg-js": "^0.8.1", @@ -619,16 +633,16 @@ } }, "node_modules/@supabase/supabase-js": { - "version": "2.112.4", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.112.4.tgz", - "integrity": "sha512-UiCX1udlFY1fQQrO7Z3GU7obQsju0w5Vk9mOOwalfo/+Gy+tahWVenSSuu5E/GTy/q//HxvGv2IrCdW66/61kw==", + "version": "2.116.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.116.0.tgz", + "integrity": "sha512-YyWmKXt2NspV9iO8FPnlswUFJIRnrLd3oTCb+3ZyYRuKZtBH0xCUDgnUqoyA0fGUxpM/UhfwDjYf/dht/9bp7g==", "license": "MIT", "dependencies": { - "@supabase/auth-js": "2.112.4", - "@supabase/functions-js": "2.112.4", - "@supabase/postgrest-js": "2.112.4", - "@supabase/realtime-js": "2.112.4", - "@supabase/storage-js": "2.112.4" + "@supabase/auth-js": "2.116.0", + "@supabase/functions-js": "2.116.0", + "@supabase/postgrest-js": "2.116.0", + "@supabase/realtime-js": "2.116.0", + "@supabase/storage-js": "2.116.0" }, "engines": { "node": ">=22.0.0" @@ -668,13 +682,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "version": "26.5.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.1.tgz", + "integrity": "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" + "undici-types": "~8.9.0" } }, "node_modules/@types/qrcode": { @@ -688,9 +702,9 @@ } }, "node_modules/@types/react": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", - "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", "dev": true, "license": "MIT", "dependencies": { @@ -698,13 +712,13 @@ } }, "node_modules/@types/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "^19.2.0" + "@types/react": "^19.3.0" } }, "node_modules/@typescript/typescript-aix-ppc64": { @@ -1077,34 +1091,17 @@ } } }, - "node_modules/@vitest/expect": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", - "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/mocker": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", - "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.1.tgz", + "integrity": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.9", + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.1", "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" + "magic-string": "^1.2.3" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1122,70 +1119,12 @@ } } }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", - "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", - "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.9", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", - "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.9", - "@vitest/utils": "4.1.9", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/spy": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", - "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", - "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.1.tgz", + "integrity": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.9", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, "funding": { "url": "https://opencollective.com/vitest" } @@ -1282,13 +1221,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -1337,6 +1269,21 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -1376,9 +1323,9 @@ "license": "MIT" }, "node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -1389,9 +1336,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, @@ -1406,9 +1353,9 @@ } }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1446,6 +1393,21 @@ "node": ">=8" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1534,21 +1496,6 @@ } } }, - "node_modules/jsdom/node_modules/whatwg-url": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.1.tgz", - "integrity": "sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.15.1", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^22.14.0 || >=24.0.0" - } - }, "node_modules/lightningcss": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", @@ -1845,13 +1792,13 @@ } }, "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz", + "integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "@jridgewell/sourcemap-codec": "^1.6.0" } }, "node_modules/mdn-data": { @@ -1862,9 +1809,9 @@ "license": "CC0-1.0" }, "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "dev": true, "funding": [ { @@ -1881,15 +1828,18 @@ } }, "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } }, "node_modules/p-limit": { "version": "2.3.0", @@ -1949,13 +1899,6 @@ "node": ">=8" } }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2071,30 +2014,30 @@ } }, "node_modules/react": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", - "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", - "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", "license": "MIT", "dependencies": { - "scheduler": "^0.27.0" + "scheduler": "^0.28.0" }, "peerDependencies": { - "react": "^19.2.8" + "react": "^19.3.0" } }, "node_modules/react-router": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.3.tgz", - "integrity": "sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", + "integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -2114,12 +2057,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.3.tgz", - "integrity": "sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.4.tgz", + "integrity": "sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==", "license": "MIT", "dependencies": { - "react-router": "7.18.3" + "react-router": "7.18.4" }, "engines": { "node": ">=20.0.0" @@ -2155,13 +2098,13 @@ "license": "ISC" }, "node_modules/rolldown": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.7.tgz", - "integrity": "sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", + "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.148.0", + "@oxc-project/types": "=0.149.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -2171,21 +2114,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.7", - "@rolldown/binding-android-arm64": "1.2.7", - "@rolldown/binding-darwin-arm64": "1.2.7", - "@rolldown/binding-darwin-x64": "1.2.7", - "@rolldown/binding-freebsd-x64": "1.2.7", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.7", - "@rolldown/binding-linux-arm64-gnu": "1.2.7", - "@rolldown/binding-linux-arm64-musl": "1.2.7", - "@rolldown/binding-linux-ppc64-gnu": "1.2.7", - "@rolldown/binding-linux-s390x-gnu": "1.2.7", - "@rolldown/binding-linux-x64-gnu": "1.2.7", - "@rolldown/binding-linux-x64-musl": "1.2.7", - "@rolldown/binding-openharmony-arm64": "1.2.7", - "@rolldown/binding-win32-arm64-msvc": "1.2.7", - "@rolldown/binding-win32-x64-msvc": "1.2.7" + "@rolldown/binding-android-arm-eabi": "1.2.8", + "@rolldown/binding-android-arm64": "1.2.8", + "@rolldown/binding-darwin-arm64": "1.2.8", + "@rolldown/binding-darwin-x64": "1.2.8", + "@rolldown/binding-freebsd-x64": "1.2.8", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", + "@rolldown/binding-linux-arm64-gnu": "1.2.8", + "@rolldown/binding-linux-arm64-musl": "1.2.8", + "@rolldown/binding-linux-ppc64-gnu": "1.2.8", + "@rolldown/binding-linux-s390x-gnu": "1.2.8", + "@rolldown/binding-linux-x64-gnu": "1.2.8", + "@rolldown/binding-linux-x64-musl": "1.2.8", + "@rolldown/binding-openharmony-arm64": "1.2.8", + "@rolldown/binding-win32-arm64-msvc": "1.2.8", + "@rolldown/binding-win32-x64-msvc": "1.2.8" } }, "node_modules/saxes": { @@ -2202,9 +2145,9 @@ } }, "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", "license": "MIT" }, "node_modules/set-blocking": { @@ -2244,9 +2187,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -2284,16 +2227,19 @@ "license": "MIT" }, "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz", + "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } }, "node_modules/tinyexec": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", - "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", "dev": true, "license": "MIT", "engines": { @@ -2317,33 +2263,23 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/tldts": { - "version": "7.4.12", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.12.tgz", - "integrity": "sha512-WylhSDKVeYnWXL3a+vKTaOxjnOeEGw938hImY8zoRWJjRRK/Jp1K+IihBzIONpUmW4e3WmXT6q5FW6vlESVZCA==", + "version": "7.4.13", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.13.tgz", + "integrity": "sha512-iHtaIWWIbMDkCeJdTBzZFGgbluE5J+oHlb2g7+oAz1S1gpuVpabRZdQyd471Vl8UUkcz2vXSL8xZH2kyCe8tfA==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.4.12" + "tldts-core": "^7.4.13" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.4.12", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.12.tgz", - "integrity": "sha512-nYNzS2WRf4QJmjzFFgAxLOBjyBxAGRbCy9PVBPaglcYyYajh40VBn+v5Ngr96ZMc7oM0+aCJdtQnNejvdBnXMQ==", + "version": "7.4.13", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.13.tgz", + "integrity": "sha512-mbYsrih5FRtGxs3Usvl/PqwJsNpp+jsmrdFviiK02teHDG0/HebBG/pqCylje3kzgXYzuLoHJF/0mz9W53t8Xg==", "dev": true, "license": "MIT" }, @@ -2425,23 +2361,23 @@ } }, "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", "dev": true, "license": "MIT" }, "node_modules/vite": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", - "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.26", - "rolldown": "~1.2.4", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "bin": { @@ -2458,7 +2394,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -2509,54 +2445,32 @@ } } }, - "node_modules/vite/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/vitest": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", - "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.1.tgz", + "integrity": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.9", - "@vitest/mocker": "4.1.9", - "@vitest/pretty-format": "4.1.9", - "@vitest/runner": "4.1.9", - "@vitest/snapshot": "4.1.9", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.1", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "6.1.4", + "tinyexec": "1.3.0", + "tinyglobby": "^0.2.17", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -2564,16 +2478,16 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.9", - "@vitest/browser-preview": "4.1.9", - "@vitest/browser-webdriverio": "4.1.9", - "@vitest/coverage-istanbul": "4.1.9", - "@vitest/coverage-v8": "4.1.9", - "@vitest/ui": "4.1.9", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.1", + "@vitest/browser-preview": "5.0.1", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.1", + "@vitest/coverage-v8": "5.0.1", + "@vitest/ui": "5.0.1", "happy-dom": "*", "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -2648,18 +2562,18 @@ } }, "node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.1.tgz", + "integrity": "sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==", "dev": true, "license": "MIT", "dependencies": { - "@exodus/bytes": "^1.11.0", + "@exodus/bytes": "^1.15.1", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.14.0 || >=24.0.0" } }, "node_modules/which-module": { diff --git a/webapp/package.json b/webapp/package.json index 6c066cd..112617a 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -14,21 +14,21 @@ "test:all": "npm test && npm run test:e2e" }, "dependencies": { - "@supabase/supabase-js": "^2.112.4", + "@supabase/supabase-js": "^2.116.0", "qrcode": "^1.5.4", - "react": "^19.2.7", - "react-dom": "^19.2.8", - "react-router-dom": "^7.18.3" + "react": "^19.3.0", + "react-dom": "^19.3.0", + "react-router-dom": "^7.18.4" }, "devDependencies": { "@playwright/test": "^1.63.0", "@types/qrcode": "^1.5.6", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.7", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", "@vitejs/plugin-react": "^6.1.1", "jsdom": "^30.0.1", "typescript": "^7.0.2", - "vite": "^8.2.2", - "vitest": "^4.1.9" + "vite": "^8.3.0", + "vitest": "^5.0.1" } } diff --git a/webapp/src/CameraCapture.ts b/webapp/src/CameraCapture.ts index 68f71ea..f37593b 100644 --- a/webapp/src/CameraCapture.ts +++ b/webapp/src/CameraCapture.ts @@ -19,7 +19,13 @@ export async function startCamera(): Promise { video.srcObject = stream; video.playsInline = true; video.muted = true; - await video.play(); + try { + await video.play(); + } catch (error) { + stream.getTracks().forEach(track => track.stop()); + video.srcObject = null; + throw error; + } logger.info('Camera', 'Video element playing', { width: video.videoWidth, height: video.videoHeight }); const PHOTO_WIDTH = 1080; diff --git a/webapp/src/CaptainClient.ts b/webapp/src/CaptainClient.ts index 54f476f..3c771c9 100644 --- a/webapp/src/CaptainClient.ts +++ b/webapp/src/CaptainClient.ts @@ -21,6 +21,7 @@ function uuid(): string { export class CaptainClient { private bootstrap?: SessionBootstrap; + private generation = 0; private realtimeSub?: ReturnType; private listeners = new Set(); private state: CaptainState; @@ -44,16 +45,19 @@ export class CaptainClient { private notify() { for (const l of this.listeners) l(this.state); } async startSession(displayName: string) { + const generation = ++this.generation; logger.info('CaptainClient', 'Starting session', { displayName }); this.state = { ...this.state, status: 'creating' }; this.notify(); try { - this.bootstrap = await createSession({ + const bootstrap = await createSession({ hostName: displayName, anonymousId: this.participantId, peerId: this.participantId, }); + if (generation !== this.generation) return; + this.bootstrap = bootstrap; logger.info('CaptainClient', 'Session created', { code: this.bootstrap.session.code, id: this.bootstrap.session.id }); this.state = { @@ -81,7 +85,7 @@ export class CaptainClient { createdAt: new Date().toISOString(), expiresAt: this.bootstrap.session.expires_at ?? new Date(Date.now() + 600_000).toISOString(), timerDuration: 10, - triggerPermission: 'everyoneCanStartTimer', + triggerPermission: 'hostOnly', isDiscoverableNearby: false, allowWebJoin: true, allowFinalPhotoDownload: true, @@ -143,6 +147,8 @@ export class CaptainClient { } stop() { + this.generation++; + this.bootstrap = undefined; logger.info('CaptainClient', 'Stopping'); this.realtimeSub?.unsubscribe(); this.listeners.clear(); diff --git a/webapp/src/DesignLabels.ts b/webapp/src/DesignLabels.ts index a24f7ca..abc4d24 100644 --- a/webapp/src/DesignLabels.ts +++ b/webapp/src/DesignLabels.ts @@ -5,6 +5,20 @@ */ export const DesignLabels = { + pirateJokes: [ + "Why is pirating so addictive? Lose one hand and ye get hooked!", + "What's a pirate's fav letter? Ye think it's R — but it's the C!", + "How much did peg leg and hook cost? An arm and a leg!", + "What d'ye call a pirate who skips class? Captain Hooky!", + "Why couldn't the pirate play cards? He was standing on the deck!", + "What's a pirate's fav country? ARRRgentina!", + "What did the ocean say to the pirate? Nothing — it just waved!", + ], + appNameFirstLine: 'All Hands', + appNameSecondLine: 'On Deck', + webHostingHint: 'Crew members can join in a browser using your session code or QR code.', + loading: 'Loading…', + browserCapturePolicy: 'The captain controls the timer and shutter in browser-hosted sessions.', // Buttons cancel: 'Cancel', close: 'Close', diff --git a/webapp/src/HomePage.tsx b/webapp/src/HomePage.tsx index 82da980..2f8571f 100644 --- a/webapp/src/HomePage.tsx +++ b/webapp/src/HomePage.tsx @@ -1,36 +1,21 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { DesignLabels } from './DesignLabels'; - -const pirateJokes = [ - "Why is pirating so addictive? Lose one hand and ye get hooked!", - "What's a pirate's fav letter? Ye think it's R — but it's the C!", - "How much did peg leg and hook cost? An arm and a leg!", - "What d'ye call a pirate who skips class? Captain Hooky!", - "Why couldn't the pirate play cards? He was standing on the deck!", - "What's a pirate's fav country? ARRRgentina!", - "What did the ocean say to the pirate? Nothing — it just waved!", -]; +import { parseJoinTarget } from './joinTarget'; export function HomePage() { const navigate = useNavigate(); const [code, setCode] = useState(''); const [jokeIndex, setJokeIndex] = useState(0); - const [webJoin, setWebJoin] = useState(false); const [activeTab, setActiveTab] = useState<'join' | 'host'>('join'); useEffect(() => { - setJokeIndex(Math.floor(Math.random() * pirateJokes.length)); + setJokeIndex(Math.floor(Math.random() * DesignLabels.pirateJokes.length)); }, []); - const extractCode = (input: string): string => { - const match = input.match(/(?:join\/)?([A-Z0-9]{6,10})/i); - return match ? match[1].toUpperCase() : input.toUpperCase().replace(/[^A-Z0-9]/g, ''); - }; - + const joinTarget = parseJoinTarget(code); const handleJoin = () => { - const clean = extractCode(code); - if (clean.length >= 6) navigate(`/join/${clean}`); + if (joinTarget) navigate(joinTarget); }; return ( @@ -52,8 +37,8 @@ export function HomePage() { {/* Title */}

- All Hands - On Deck + {DesignLabels.appNameFirstLine} + {DesignLabels.appNameSecondLine}

{DesignLabels.homeSubtitle.split('\n')[0]} @@ -64,12 +49,14 @@ export function HomePage() { @@ -82,7 +69,9 @@ export function HomePage() { className="id-input id-input-glow" placeholder={DesignLabels.sessionCodePlaceholder} value={code} - onChange={e => setCode(e.target.value.toUpperCase())} + onChange={e => setCode(/^[a-z0-9]*$/i.test(e.target.value) ? e.target.value.toUpperCase() : e.target.value)} + aria-label={DesignLabels.hostSessionCode} + data-testid="session-code" autoCapitalize="characters" autoCorrect="off" autoComplete="off" @@ -91,9 +80,9 @@ export function HomePage() { /> @@ -107,20 +96,7 @@ export function HomePage() { 📷 {DesignLabels.startCrewPhoto} -

- 🌐 -
-
- {DesignLabels.allowWebViewers} - {DesignLabels.betaBadge} -
- {webJoin ? 'On — web join enabled' : 'Off — nearby only'} -
-
setWebJoin(o => !o)} - style={{ background: webJoin ? 'linear-gradient(135deg, var(--gold), var(--amber))' : 'rgba(255,255,255,0.15)' }}> -
-
-
+

{DesignLabels.webHostingHint}

)} @@ -130,15 +106,15 @@ export function HomePage() { {/* Floating speech bubble for Pirate Joke */}
🏴‍☠️ - "{pirateJokes[jokeIndex]}" + "{DesignLabels.pirateJokes[jokeIndex]}"

- {DesignLabels.privacy} + {DesignLabels.privacy} {' · '} - {DesignLabels.imprint} + {DesignLabels.imprint} {' · '} - v2.4.3 + v2.4.4

); diff --git a/webapp/src/HostPage.tsx b/webapp/src/HostPage.tsx index f638bfc..9eea45b 100644 --- a/webapp/src/HostPage.tsx +++ b/webapp/src/HostPage.tsx @@ -6,7 +6,6 @@ import { DesignLabels } from './DesignLabels'; import { QRCodePanel } from './components/QRCodePanel'; const TIMER_OPTIONS = [5, 10, 20, 30]; -type TriggerPermission = 'captainOnly' | 'everyoneCanStartTimer' | 'viewersCanRequest'; export function CaptainPage() { const navigate = useNavigate(); @@ -19,10 +18,10 @@ export function CaptainPage() { const [showQR, setShowQR] = useState(true); const [showSettings, setShowSettings] = useState(false); const [timerDuration, setTimerDuration] = useState(10); - const [triggerPermission, setTriggerPermission] = useState('everyoneCanStartTimer'); const [jpegQuality, setJpegQuality] = useState(0.3); const [frameWidth, setFrameWidth] = useState(240); const videoRef = useRef(null); + const countdownGeneration = useRef(0); const client = useMemo(() => new CaptainClient(), []); @@ -40,13 +39,18 @@ export function CaptainPage() { // Auto-start session immediately on mount useEffect(() => { + let cancelled = false; + let ownedCamera: CameraCapture | undefined; void (async () => { - if (state !== null) return; try { const cam = await startCamera(); + if (cancelled) { cam.stop(); return; } + ownedCamera = cam; setCamera(cam); - await client.startSession('Captain'); + await client.startSession(DesignLabels.captain); } catch (e: unknown) { + ownedCamera?.stop(); + if (cancelled) return; if (e instanceof DOMException && e.name === 'NotAllowedError') { setCamError('Camera access denied.'); } else if (e instanceof DOMException && e.name === 'NotFoundError') { @@ -56,13 +60,16 @@ export function CaptainPage() { } } })(); - }, []); // eslint-disable-line react-hooks/exhaustive-deps + return () => { cancelled = true; countdownGeneration.current++; ownedCamera?.stop(); }; + }, [client]); const capturePhoto = useCallback(async () => { if (!camera || !state || state.status !== 'active') return; + const generation = ++countdownGeneration.current; setCountdown(timerDuration); for (let i = timerDuration - 1; i >= 0; i--) { await new Promise(r => setTimeout(r, 1000)); + if (generation !== countdownGeneration.current) return; setCountdown(i); } setCountdown(null); @@ -128,7 +135,7 @@ export function CaptainPage() { - + {countdown !== null && countdown > 0 &&
{countdown > 9 ? '' : countdown}
}
@@ -142,7 +149,7 @@ export function CaptainPage() { {!state.finalPhotoBase64 && !showSettings && (
{countdown !== null ? ( - + ) : (
@@ -177,16 +184,7 @@ export function CaptainPage() { ))}
-
-

Trigger Permission

-
- {(['captainOnly', 'everyoneCanStartTimer', 'viewersCanRequest'] as const).map(p => ( - - ))} -
-
+

{DesignLabels.browserCapturePolicy}

Quality

diff --git a/webapp/src/JoinPage.tsx b/webapp/src/JoinPage.tsx index d790754..c39b8d5 100644 --- a/webapp/src/JoinPage.tsx +++ b/webapp/src/JoinPage.tsx @@ -304,7 +304,7 @@ function DebugPanel({ open, onToggle }: { open: boolean; onToggle: () => void }) {open && ( diff --git a/webapp/src/__tests__/hostClient.test.ts b/webapp/src/__tests__/hostClient.test.ts index 3870809..1edfa6b 100644 --- a/webapp/src/__tests__/hostClient.test.ts +++ b/webapp/src/__tests__/hostClient.test.ts @@ -78,6 +78,13 @@ describe('CaptainClient', () => { expect(activeState.sessionId).toBe('uuid-123'); }); + it('advertises the supported captain-only capture policy', async () => { + await client.startSession('Captain'); + const metadata = insertSpy.mock.calls.map(([row]) => row as any) + .find(row => row.type === 'sessionMetadata').payload.event.sessionMetadata; + expect(metadata.triggerPermission).toBe('hostOnly'); + }); + it('sets finalPhotoBase64 after sendFinalPhoto', async () => { await client.startSession('Captain'); diff --git a/webapp/src/__tests__/hostLifecycle.test.ts b/webapp/src/__tests__/hostLifecycle.test.ts new file mode 100644 index 0000000..6a91ab6 --- /dev/null +++ b/webapp/src/__tests__/hostLifecycle.test.ts @@ -0,0 +1,28 @@ +import { afterEach, expect, it, vi } from 'vitest'; +import { CaptainClient } from '../CaptainClient'; +import { startCamera } from '../CameraCapture'; +const mocks = vi.hoisted(() => ({ create: vi.fn(), subscribe: vi.fn(), insert: vi.fn() })); +vi.mock('../lib/logger', () => ({ logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn() } })); +vi.mock('../lib/supabase', () => ({ getSupabaseClient: () => ({ from: () => ({ insert: mocks.insert }) }) })); +vi.mock('../services/sessionService', () => ({ createSession: mocks.create })); +vi.mock('../services/realtimeService', () => ({ subscribeToSessionRealtime: mocks.subscribe })); +afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); vi.clearAllMocks(); }); +it('does not subscribe when session creation completes after stop', async () => { + let resolve!: (value: unknown) => void; + mocks.create.mockReturnValue(new Promise(r => { resolve = r; })); + const client = new CaptainClient(); + const pending = client.startSession('Captain'); + client.stop(); + resolve({ session: { id: 'id', code: 'ABC123' }, participant: { id: 'p' } }); + await pending; + expect(mocks.subscribe).not.toHaveBeenCalled(); + expect(mocks.insert).not.toHaveBeenCalled(); +}); +it('releases camera tracks if video playback fails during initialization', async () => { + const stop = vi.fn(); + vi.stubGlobal('navigator', { mediaDevices: { getUserMedia: vi.fn().mockResolvedValue({ getTracks: () => [{ stop }] }) } }); + vi.spyOn(HTMLMediaElement.prototype, 'play').mockRejectedValue(new Error('playback failed')); + vi.spyOn(HTMLMediaElement.prototype, 'pause').mockImplementation(() => {}); + await expect(startCamera()).rejects.toThrow('playback failed'); + expect(stop).toHaveBeenCalledOnce(); +}); diff --git a/webapp/src/__tests__/hostPage.test.tsx b/webapp/src/__tests__/hostPage.test.tsx new file mode 100644 index 0000000..7b63767 --- /dev/null +++ b/webapp/src/__tests__/hostPage.test.tsx @@ -0,0 +1,80 @@ +import { act } from 'react'; +import { createRoot } from 'react-dom/client'; +import { MemoryRouter } from 'react-router-dom'; +import { afterEach, beforeEach, expect, it, vi } from 'vitest'; +import { CaptainPage } from '../HostPage'; + +const mocks = vi.hoisted(() => ({ + stop: vi.fn(), captureFrame: vi.fn(() => 'jpeg'), sendPreviewFrame: vi.fn(), + startCamera: vi.fn(), startSession: vi.fn(), capturePhoto: vi.fn(() => "photo"), sendFinalPhoto: vi.fn(), +})); +vi.mock('../CameraCapture', () => ({ startCamera: mocks.startCamera })); +vi.mock('../components/QRCodePanel', () => ({ QRCodePanel: () => null })); +vi.mock('../CaptainClient', () => ({ CaptainClient: class { + subscribe(listener: (state: unknown) => void) { + listener({ status: 'active', sessionCode: 'ABC123', participants: [] }); + return () => {}; + } + stop() {} + startSession = mocks.startSession; + sendPreviewFrame = mocks.sendPreviewFrame; + sendFinalPhoto = mocks.sendFinalPhoto; +} })); +let container: HTMLDivElement; +let root: ReturnType; +beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }); + vi.spyOn(HTMLMediaElement.prototype, 'play').mockResolvedValue(); + container = document.createElement('div'); + document.body.append(container); + root = createRoot(container); + mocks.startCamera.mockResolvedValue({ stream: {}, stop: mocks.stop, captureFrame: mocks.captureFrame, capturePhoto: mocks.capturePhoto }); +}); +afterEach(async () => { + await act(async () => root.unmount()); + container.remove(); + vi.useRealTimers(); + vi.restoreAllMocks(); +}); +const mount = () => act(async () => { root.render(); }); +it('stops the camera when the host page unmounts', async () => { + await mount(); + await act(async () => root.unmount()); + expect(mocks.stop).toHaveBeenCalled(); +}); +it('applies preview quality and size settings to captured frames', async () => { + await mount(); + await act(async () => container.querySelector('[aria-label="Settings"]')!.click()); + for (const label of ['70%', '480p']) { + await act(async () => Array.from(container.querySelectorAll('button')).find(b => b.textContent === label)!.click()); + } + await act(async () => vi.advanceTimersByTime(334)); + expect(mocks.captureFrame).toHaveBeenLastCalledWith(0.7, 480); +}); +it('stops a late camera result without creating a session after navigation', async () => { + let resolve!: (camera: unknown) => void; + mocks.startCamera.mockReturnValue(new Promise(r => { resolve = r; })); + await mount(); + await act(async () => root.unmount()); + await act(async () => resolve({ stream: {}, stop: mocks.stop })); + expect(mocks.stop).toHaveBeenCalled(); + expect(mocks.startSession).not.toHaveBeenCalled(); +}); + +it('cancelling the countdown prevents capture', async () => { + await mount(); + const button = (label: string) => Array.from(container.querySelectorAll('button')).find(b => b.textContent?.includes(label))!; + await act(async () => button('Start 10s').click()); + await act(async () => button('Cancel').click()); + await act(async () => vi.advanceTimersByTimeAsync(11000)); + expect(mocks.capturePhoto).not.toHaveBeenCalled(); + expect(mocks.sendFinalPhoto).not.toHaveBeenCalled(); +}); +it('shows the supported captain-only trigger policy', async () => { + await mount(); + await act(async () => container.querySelector('[aria-label="Settings"]')!.click()); + expect(container.textContent).toContain('The captain controls the timer and shutter in browser-hosted sessions.'); + expect(container.textContent).not.toContain('Pirate can trigger'); +}); diff --git a/webapp/src/joinTarget.ts b/webapp/src/joinTarget.ts new file mode 100644 index 0000000..5f35587 --- /dev/null +++ b/webapp/src/joinTarget.ts @@ -0,0 +1,17 @@ +const SESSION_CODE = /^[A-Z0-9]{6,10}$/i; + +/** Convert a code or invite to a local route without changing token case. */ +export function parseJoinTarget(input: string): string | null { + const value = input.trim(); + if (SESSION_CODE.test(value)) return `/join/${value.toUpperCase()}`; + if (!/^https?:\/\//i.test(value) && !value.startsWith('/join/')) return null; + try { + const url = new URL(value, 'https://join.invalid'); + const match = url.pathname.match(/^\/join\/([A-Z0-9]{6,10})\/?$/i); + if (!match) return null; + // Navigate locally, even when the invitation came from another host. + return `/join/${match[1].toUpperCase()}${url.search}`; + } catch { + return null; + } +} diff --git a/webapp/src/main.tsx b/webapp/src/main.tsx index 5cac71c..dbfe535 100644 --- a/webapp/src/main.tsx +++ b/webapp/src/main.tsx @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { logger } from './lib/logger'; import './styles.css'; +import { DesignLabels } from './DesignLabels'; // Route-level code splitting: the landing page must not pull in the // Supabase/session/camera code that only /host and /join need. @@ -22,7 +23,7 @@ window.addEventListener('unhandledrejection', (e) => { ReactDOM.createRoot(document.getElementById('root')!).render( - + {DesignLabels.loading}
}> } /> } /> diff --git a/webapp/src/styles.css b/webapp/src/styles.css index d4b3265..158bfcb 100644 --- a/webapp/src/styles.css +++ b/webapp/src/styles.css @@ -883,3 +883,17 @@ a { color: var(--gold); } box-shadow: 0 8px 30px rgba(243,189,46,0.45); } + +/* Keep keyboard focus visible and respect the device motion preference. */ +:where(button, a, input):focus-visible { + outline: 2px solid var(--gold); + outline-offset: 4px; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation: none !important; + transition: none !important; + scroll-behavior: auto !important; + } +}