From 5cae1bbafdf1a732d9d40fda2c7a707ae9c5e9f9 Mon Sep 17 00:00:00 2001 From: AndroLLM Date: Mon, 21 Sep 2026 22:04:37 +0530 Subject: [PATCH 1/2] Web dashboard system: Firebase identity, app-to-web connector, gated analytics sync, dashboard UI, login --- .gitignore | 6 + app/build.gradle.kts | 13 + .../io/androllm/app/AndroLLMApplication.kt | 15 + .../app/identity/FirebaseIdTokenProvider.kt | 42 + .../androllm/app/identity/IdentityModule.kt | 60 + .../io/androllm/app/sync/AnalyticsMapper.kt | 176 + .../io/androllm/app/sync/AnalyticsSync.kt | 215 + .../androllm/app/sync/AnalyticsSyncTrigger.kt | 90 + .../androllm/app/sync/AnalyticsMapperTest.kt | 157 + .../app/sync/AnalyticsSyncTriggerTest.kt | 78 + .../core/cloud/usage/CloudUsageMeter.kt | 9 + .../usage/CloudUsageMeterListenerTest.kt | 58 + .../core/datastore/PreferencesDataStore.kt | 70 + .../core/network/identity/IdentityApi.kt | 167 + .../androllm/core/network/identity/SyncApi.kt | 168 + .../core/network/identity/UserProfile.kt | 58 + .../identity/SyncApiSerializationTest.kt | 74 + .../identity/UserProfileSerializationTest.kt | 79 + .../core/telemetry/TelemetryRepository.kt | 8 + .../core/telemetry/TelemetryListenerTest.kt | 92 + documentation/backend/identity-foundation.md | 225 + documentation/backend/schema-identity.sql | 35 + feature/settings/build.gradle.kts | 1 + .../feature/settings/SettingsAccordion.kt | 3 +- .../feature/settings/SettingsScreen.kt | 150 + .../feature/settings/WebDashboardViewModel.kt | 190 + website/.dockerignore | 21 +- website/.env.example | 13 + website/.gitignore | 7 +- website/Dockerfile | 61 +- website/app/account/page.tsx | 168 + website/app/dashboard/page.tsx | 15 + website/app/dashboard/screen.test.tsx | 93 + website/app/dashboard/screen.tsx | 458 ++ website/app/login/page.tsx | 11 + website/app/login/screen.tsx | 27 + website/components/dashboard/activity.tsx | 113 + website/components/dashboard/charts.tsx | 159 + website/components/dashboard/compare.tsx | 173 + website/components/dashboard/kpi.test.tsx | 49 + website/components/dashboard/kpi.tsx | 133 + website/components/dashboard/patterns.tsx | 359 ++ website/components/dashboard/tables.tsx | 156 + website/components/layout/navbar.tsx | 9 + website/components/ui/sign-up.test.tsx | 49 + website/components/ui/sign-up.tsx | 506 ++ website/hooks/use-backend-user.ts | 70 + website/hooks/use-dashboard.ts | 128 + website/lib/backend-client.ts | 102 + website/lib/dashboard-client.test.ts | 62 + website/lib/dashboard-client.ts | 157 + website/lib/dashboard-format.test.ts | 143 + website/lib/dashboard-format.ts | 218 + website/lib/dashboard-shapes.test.ts | 42 + website/lib/firebase-client.ts | 96 + website/lib/site.ts | 1 + website/package-lock.json | 5674 +++++++++++++---- website/package.json | 12 +- website/vitest.config.ts | 16 + website/vitest.setup.ts | 31 + 60 files changed, 10415 insertions(+), 1156 deletions(-) create mode 100644 app/src/main/java/io/androllm/app/identity/FirebaseIdTokenProvider.kt create mode 100644 app/src/main/java/io/androllm/app/identity/IdentityModule.kt create mode 100644 app/src/main/java/io/androllm/app/sync/AnalyticsMapper.kt create mode 100644 app/src/main/java/io/androllm/app/sync/AnalyticsSync.kt create mode 100644 app/src/main/java/io/androllm/app/sync/AnalyticsSyncTrigger.kt create mode 100644 app/src/test/java/io/androllm/app/sync/AnalyticsMapperTest.kt create mode 100644 app/src/test/java/io/androllm/app/sync/AnalyticsSyncTriggerTest.kt create mode 100644 core/cloud/src/test/java/io/androllm/core/cloud/usage/CloudUsageMeterListenerTest.kt create mode 100644 core/network/src/main/java/io/androllm/core/network/identity/IdentityApi.kt create mode 100644 core/network/src/main/java/io/androllm/core/network/identity/SyncApi.kt create mode 100644 core/network/src/main/java/io/androllm/core/network/identity/UserProfile.kt create mode 100644 core/network/src/test/java/io/androllm/core/network/identity/SyncApiSerializationTest.kt create mode 100644 core/network/src/test/java/io/androllm/core/network/identity/UserProfileSerializationTest.kt create mode 100644 core/telemetry/src/test/java/io/androllm/core/telemetry/TelemetryListenerTest.kt create mode 100644 documentation/backend/identity-foundation.md create mode 100644 documentation/backend/schema-identity.sql create mode 100644 feature/settings/src/main/java/io/androllm/feature/settings/WebDashboardViewModel.kt create mode 100644 website/.env.example create mode 100644 website/app/account/page.tsx create mode 100644 website/app/dashboard/page.tsx create mode 100644 website/app/dashboard/screen.test.tsx create mode 100644 website/app/dashboard/screen.tsx create mode 100644 website/app/login/page.tsx create mode 100644 website/app/login/screen.tsx create mode 100644 website/components/dashboard/activity.tsx create mode 100644 website/components/dashboard/charts.tsx create mode 100644 website/components/dashboard/compare.tsx create mode 100644 website/components/dashboard/kpi.test.tsx create mode 100644 website/components/dashboard/kpi.tsx create mode 100644 website/components/dashboard/patterns.tsx create mode 100644 website/components/dashboard/tables.tsx create mode 100644 website/components/ui/sign-up.test.tsx create mode 100644 website/components/ui/sign-up.tsx create mode 100644 website/hooks/use-backend-user.ts create mode 100644 website/hooks/use-dashboard.ts create mode 100644 website/lib/backend-client.ts create mode 100644 website/lib/dashboard-client.test.ts create mode 100644 website/lib/dashboard-client.ts create mode 100644 website/lib/dashboard-format.test.ts create mode 100644 website/lib/dashboard-format.ts create mode 100644 website/lib/dashboard-shapes.test.ts create mode 100644 website/lib/firebase-client.ts create mode 100644 website/vitest.config.ts create mode 100644 website/vitest.setup.ts diff --git a/.gitignore b/.gitignore index cf3bb935..aabd62a2 100644 --- a/.gitignore +++ b/.gitignore @@ -225,3 +225,9 @@ tools/proot-fetch/ # set QAIRT_ROOT=C:\Qualcomm\AIStack\QAIRT # ./gradlew :imagegen:syncQnnRuntimeLibs +# --------------------------------------------------------------------------- +# Env templates (placeholders only, no secrets) — keep tracked +# --------------------------------------------------------------------------- +!.env.example +!**/.env.example + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a86eff3c..150e1569 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,6 +58,14 @@ android { useSupportLibrary = true } + // Phase 1 identity: private backend base URL (e.g. https://api.androllm.app). + // Fed by ANDROLLM_BACKEND_URL env / Gradle property; empty = no backend. + // Never put secrets here — only the public base URL. + val backendUrl = System.getenv("ANDROLLM_BACKEND_URL") + ?: project.findProperty("ANDROLLM_BACKEND_URL")?.toString() + ?: "" + buildConfigField("String", "BACKEND_BASE_URL", "\"$backendUrl\"") + ndk { abiFilters += listOf("arm64-v8a") } @@ -132,6 +140,7 @@ dependencies { implementation(project(":core:models")) implementation(project(":core:network")) implementation(project(":core:cloud")) + implementation(project(":core:telemetry")) implementation(project(":core:memory")) implementation(project(":core:voice")) implementation(project(":core:tools")) @@ -191,6 +200,10 @@ dependencies { ksp("com.google.dagger:hilt-android-compiler:2.57.1") implementation(libs.androidx.hilt) ksp("androidx.hilt:hilt-compiler:1.2.0") + + // WorkManager (analytics sync worker; same artifacts as core:memory) + implementation("androidx.hilt:hilt-work:1.2.0") + implementation("androidx.work:work-runtime-ktx:2.9.1") // Room implementation(libs.room.runtime) diff --git a/app/src/main/java/io/androllm/app/AndroLLMApplication.kt b/app/src/main/java/io/androllm/app/AndroLLMApplication.kt index c2de82f1..e6142d9c 100644 --- a/app/src/main/java/io/androllm/app/AndroLLMApplication.kt +++ b/app/src/main/java/io/androllm/app/AndroLLMApplication.kt @@ -39,6 +39,12 @@ class AndroLLMApplication : Application(), WorkConfiguration.Provider { @Inject lateinit var memoryBackgroundScheduler: MemoryBackgroundScheduler + @Inject + lateinit var analyticsSyncScheduler: io.androllm.app.sync.AnalyticsSyncScheduler + + @Inject + lateinit var analyticsSyncTrigger: io.androllm.app.sync.AnalyticsSyncTrigger + override val workManagerConfiguration: WorkConfiguration get() = WorkConfiguration.Builder().setWorkerFactory(workerFactory).build() @@ -67,6 +73,15 @@ class AndroLLMApplication : Application(), WorkConfiguration.Provider { // memoryBackgroundScheduler.schedule() deferred to MemoryRepository first write // Keep NPU env seeding on critical path (must be before first dlopen) — already done above + // Analytics sync — cheap unique-work enqueue (KEEP); the worker itself + // no-ops unless signed in AND web-connected. No data leaves the device before approval. + runCatching { analyticsSyncScheduler.schedule() } + .onFailure { Timber.w("[AnalyticsSync] schedule failed: ${it.message}") } + // Near-real-time path: each recorded request/generation enqueues a + // debounced upload (~8s), so the dashboard reflects usage in <10s. + runCatching { analyticsSyncTrigger.bind() } + .onFailure { Timber.w("[AnalyticsSync] trigger bind failed: ${it.message}") } + registerComponentCallbacks(object : ComponentCallbacks2 { override fun onTrimMemory(level: Int) { when (level) { diff --git a/app/src/main/java/io/androllm/app/identity/FirebaseIdTokenProvider.kt b/app/src/main/java/io/androllm/app/identity/FirebaseIdTokenProvider.kt new file mode 100644 index 00000000..93a2b258 --- /dev/null +++ b/app/src/main/java/io/androllm/app/identity/FirebaseIdTokenProvider.kt @@ -0,0 +1,42 @@ +package io.androllm.app.identity + +import com.google.firebase.auth.FirebaseAuth +import kotlinx.coroutines.suspendCancellableCoroutine +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.coroutines.resume + +/** + * Phase 1 — Firebase ID token source for backend calls. + * + * Wraps `FirebaseAuth.currentUser.getIdToken()` in a suspend function so + * [io.androllm.core.network.identity.IdentityApi] stays Firebase-free. + * Returns null when signed out or when Firebase is unavailable (guest mode). + */ +@Singleton +class FirebaseIdTokenProvider @Inject constructor() { + + suspend fun getToken(forceRefresh: Boolean = false): String? { + val auth = runCatching { FirebaseAuth.getInstance() }.getOrNull() ?: return null + val user = auth.currentUser ?: return null + return try { + suspendCancellableCoroutine { cont -> + user.getIdToken(forceRefresh) + .addOnSuccessListener { result -> + cont.resume(result.token) + } + .addOnFailureListener { e -> + Timber.w(e, "[Identity] getIdToken failed") + cont.resume(null) + } + } + } catch (e: Exception) { + Timber.w(e, "[Identity] getIdToken threw") + null + } + } + + fun isSignedIn(): Boolean = + runCatching { FirebaseAuth.getInstance().currentUser != null }.getOrDefault(false) +} diff --git a/app/src/main/java/io/androllm/app/identity/IdentityModule.kt b/app/src/main/java/io/androllm/app/identity/IdentityModule.kt new file mode 100644 index 00000000..03d17fc7 --- /dev/null +++ b/app/src/main/java/io/androllm/app/identity/IdentityModule.kt @@ -0,0 +1,60 @@ +package io.androllm.app.identity + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import io.androllm.app.BuildConfig +import io.androllm.core.network.identity.IdentityApi +import io.androllm.core.network.identity.SyncApi +import io.ktor.client.HttpClient +import javax.inject.Singleton + +/** + * Phase 1 — wires [IdentityApi] to the private backend. + * + * Base URL comes from `BuildConfig.BACKEND_BASE_URL`, which is fed by the + * `ANDROLLM_BACKEND_URL` environment variable / Gradle property at build time + * (see `app/build.gradle.kts`). Empty means "no backend configured" — the app + * works fully offline/guest and every [IdentityApi] call returns Unauthorized + * without hitting the network. + */ +@Module +@InstallIn(SingletonComponent::class) +object IdentityModule { + + @Provides + @Singleton + fun provideIdentityApi( + httpClient: HttpClient, + tokenProvider: FirebaseIdTokenProvider, + ): IdentityApi { + val baseUrl = BuildConfig.BACKEND_BASE_URL.trim() + return IdentityApi( + baseUrl = baseUrl.ifBlank { "https://localhost-disabled.invalid" }, + httpClient = httpClient, + idTokenProvider = { forceRefresh -> tokenProvider.getToken(forceRefresh) }, + isConfigured = baseUrl.isNotBlank(), + ) + } + + /** + * Phase 3 (revised) — usage/snapshot ingestion client. Same base URL and + * token source as [IdentityApi]; callers must gate on + * `UserProfile.canSyncUsage` (the worker verifies via `GET /me`). + */ + @Provides + @Singleton + fun provideSyncApi( + httpClient: HttpClient, + tokenProvider: FirebaseIdTokenProvider, + ): SyncApi { + val baseUrl = BuildConfig.BACKEND_BASE_URL.trim() + return SyncApi( + baseUrl = baseUrl.ifBlank { "https://localhost-disabled.invalid" }, + httpClient = httpClient, + idTokenProvider = { forceRefresh -> tokenProvider.getToken(forceRefresh) }, + isConfigured = baseUrl.isNotBlank(), + ) + } +} diff --git a/app/src/main/java/io/androllm/app/sync/AnalyticsMapper.kt b/app/src/main/java/io/androllm/app/sync/AnalyticsMapper.kt new file mode 100644 index 00000000..d75d7eef --- /dev/null +++ b/app/src/main/java/io/androllm/app/sync/AnalyticsMapper.kt @@ -0,0 +1,176 @@ +package io.androllm.app.sync + +import io.androllm.core.cloud.usage.CloudRequestKind +import io.androllm.core.cloud.usage.CloudUsageRecord +import io.androllm.core.cloud.usage.CloudUsageSnapshot +import io.androllm.core.network.identity.SourcePage +import io.androllm.core.network.identity.SyncEvent +import io.androllm.core.telemetry.DeviceMetrics +import io.androllm.core.telemetry.GenerationStat +import java.time.Instant +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +/** + * Phase 3 (revised) — maps the app's existing analytics surfaces onto the + * backend sync payloads. Pure functions (no Android framework beyond data + * classes) so the mapping rules are unit-testable. + * + * Sources (UI untouched — only the data these screens already render): + * - Cloud usage dashboard → [CloudUsageRecord] (stable UUID ids, persisted ring) + * - Developer page → [GenerationStat] (in-memory generation history) + */ + +private fun Long.coerceToApiInt(): Int = coerceIn(0L, Int.MAX_VALUE.toLong()).toInt() + +private fun Long.toIsoUtc(): String = Instant.ofEpochMilli(this).toString() + +private val HOUR_BUCKET_FORMAT: DateTimeFormatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd-HH").withZone(ZoneOffset.UTC) + +/** Stable hourly bucket (`2026-09-21-14`) — retries within the hour reuse the snapshot id. */ +fun snapshotBucket(sourcePage: String, nowMs: Long): String = + "$sourcePage:${HOUR_BUCKET_FORMAT.format(Instant.ofEpochMilli(nowMs))}" + +fun cloudEventType(kind: CloudRequestKind): String = when (kind) { + CloudRequestKind.CHAT -> "chat_completed" + CloudRequestKind.EMBEDDING -> "embedding_completed" + CloudRequestKind.HEALTH -> "health_check" + CloudRequestKind.MODELS -> "models_listed" +} + +/** One cloud dashboard record → one sync event. Stable id = record.id (dedup key). */ +fun CloudUsageRecord.toSyncEvent(): SyncEvent = SyncEvent( + eventId = id, + eventType = cloudEventType(kind), + sessionId = null, // Local conversation ids are not backend sessions; attaching them would reject. + sourcePage = SourcePage.CLOUD_USAGE_DASHBOARD, + modelName = modelId.ifBlank { null }, + providerName = providerName.ifBlank { null }, + engineType = "cloud", + tokensInput = inputTokens.coerceToApiInt(), + tokensOutput = outputTokens.coerceToApiInt(), + totalTokens = totalTokens.coerceToApiInt(), + latencyMs = latencyMs.coerceAtLeast(0L), + timeToFirstTokenMs = firstTokenMs?.coerceAtLeast(0L), + success = success, + errorCode = errorKind.name.takeIf { it != "NONE" }, + errorMessage = errorMessage.take(2000).ifBlank { null }, + metadata = buildJsonObject { + put("kind", kind.name) + put("cachedTokens", cachedTokens) + put("estimatedCostMicros", estimatedCostMicros) + put("retryCount", retryCount) + put("usedFallbackProvider", usedFallbackProvider) + put("cacheHit", cacheHit) + put("cacheSavedTokens", cacheSavedTokens) + put("toolCallsCount", toolCallsCount) + put("finishReason", finishReason) + put("streamed", streamed) + }, + createdAt = timestampMs.toIsoUtc(), +) + +/** Deterministic dedup key for a local generation (stable across worker retries). */ +fun generationKey(stat: GenerationStat): String = + "devgen-${stat.timestampMs}-${stat.modelName.hashCode()}-${stat.totalTokens}" + +/** + * One developer-page generation → one sync event, counted as a local chat so + * the website dashboard accrues local tokens. A generation that produced + * output counts as success; anything else preserves the stop reason. + */ +fun GenerationStat.toSyncEvent(): SyncEvent { + val succeeded = generatedTokens > 0 + return SyncEvent( + eventId = generationKey(this), + eventType = "chat_completed", + sessionId = null, + sourcePage = SourcePage.DEVELOPER_PAGE, + modelName = modelName.ifBlank { null }, + providerName = null, + engineType = "local", + tokensInput = promptTokens.coerceToApiInt(), + tokensOutput = generatedTokens.coerceToApiInt(), + totalTokens = totalTokens.coerceToApiInt(), + latencyMs = totalTimeMs.coerceAtLeast(0L), + timeToFirstTokenMs = firstTokenMs.coerceAtLeast(0L), + success = succeeded, + errorCode = if (succeeded) null else stopReason.take(128).ifBlank { "no_output" }, + errorMessage = null, + metadata = buildJsonObject { + put("tokensPerSecond", tokensPerSecond) + put("stopReason", stopReason) + }, + createdAt = timestampMs.toIsoUtc(), + ) +} + +/** + * Page-level cloud dashboard rollup. Raw records travel as events, so the + * history list is excluded here to keep the payload small. + */ +fun buildCloudSnapshotPayload(snapshot: CloudUsageSnapshot): JsonObject = buildJsonObject { + put("generatedAtMs", snapshot.generatedAtMs) + put("currentProviderId", snapshot.currentProviderId) + put("currentProviderName", snapshot.currentProviderName) + put("currentModelId", snapshot.currentModelId) + put("activeSessions", snapshot.activeSessions) + putJsonTotals("total", snapshot.total.requests, snapshot.total.inputTokens, snapshot.total.outputTokens, + snapshot.total.totalTokens, snapshot.total.successes, snapshot.total.failures, + snapshot.total.avgLatencyMs, snapshot.total.avgFirstTokenMs, snapshot.total.estimatedCostMicros) + putJsonTotals("today", snapshot.today.requests, snapshot.today.inputTokens, snapshot.today.outputTokens, + snapshot.today.totalTokens, snapshot.today.successes, snapshot.today.failures, + snapshot.today.avgLatencyMs, snapshot.today.avgFirstTokenMs, snapshot.today.estimatedCostMicros) + put("alertCount", snapshot.alerts.size) +} + +/** Page-level developer telemetry summary. */ +fun buildDeveloperSnapshotPayload( + generations: List, + deviceMetrics: DeviceMetrics?, + currentModel: String, +): JsonObject = buildJsonObject { + put("generationCount", generations.size) + put("totalTokens", generations.sumOf { it.totalTokens }) + val tps = generations.map { it.tokensPerSecond }.filter { it > 0 } + put("avgTokensPerSecond", if (tps.isEmpty()) 0.0 else tps.average()) + val lat = generations.map { it.totalTimeMs }.filter { it > 0 } + put("avgLatencyMs", if (lat.isEmpty()) 0 else lat.average().toLong()) + put("currentModel", currentModel) + if (deviceMetrics != null) { + put("deviceModel", deviceMetrics.deviceModel) + put("totalRamMb", deviceMetrics.totalRamMb) + put("usedRamMb", deviceMetrics.usedRamMb) + put("cpuCores", deviceMetrics.cpuCores) + put("freeStorageBytes", deviceMetrics.freeStorageBytes) + } +} + +private fun kotlinx.serialization.json.JsonObjectBuilder.putJsonTotals( + key: String, + requests: Int, + inputTokens: Long, + outputTokens: Long, + totalTokens: Long, + successes: Int, + failures: Int, + avgLatencyMs: Long, + avgFirstTokenMs: Long, + estimatedCostMicros: Long, +) { + put(key, buildJsonObject { + put("requests", requests) + put("inputTokens", inputTokens) + put("outputTokens", outputTokens) + put("totalTokens", totalTokens) + put("successes", successes) + put("failures", failures) + put("avgLatencyMs", avgLatencyMs) + put("avgFirstTokenMs", avgFirstTokenMs) + put("estimatedCostMicros", estimatedCostMicros) + }) +} diff --git a/app/src/main/java/io/androllm/app/sync/AnalyticsSync.kt b/app/src/main/java/io/androllm/app/sync/AnalyticsSync.kt new file mode 100644 index 00000000..9abeb484 --- /dev/null +++ b/app/src/main/java/io/androllm/app/sync/AnalyticsSync.kt @@ -0,0 +1,215 @@ +package io.androllm.app.sync + +import android.content.Context +import android.os.Build +import android.provider.Settings +import androidx.hilt.work.HiltWorker +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import com.google.firebase.auth.FirebaseAuth +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import dagger.hilt.android.qualifiers.ApplicationContext +import io.androllm.app.BuildConfig +import io.androllm.core.cloud.usage.CloudUsageMeter +import io.androllm.core.datastore.PreferencesDataStore +import io.androllm.core.network.identity.EventsBatchRequest +import io.androllm.core.network.identity.IdentityApi +import io.androllm.core.network.identity.IdentityApi.IdentityResult +import io.androllm.core.network.identity.SnapshotRequest +import io.androllm.core.network.identity.SyncApi +import io.androllm.core.network.identity.SyncDevice +import io.androllm.core.network.identity.canSyncUsage +import io.androllm.core.telemetry.TelemetryRepository +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.flow.first +import kotlinx.serialization.json.JsonObject +import timber.log.Timber + +/** + * Phase 3 (revised) — uploads analytics from the existing in-app surfaces: + * cloud usage dashboard records ([CloudUsageMeter]) and developer-page + * generations ([TelemetryRepository]), plus one page-level snapshot each. + * + * Hard gates (checked in order, cheapest first — no network until all pass): + * 1. Backend configured, Firebase signed in, cached `web_connected`. + * 2. Fresh `GET /me` confirms [canSyncUsage]; anything else stops quietly. + * + * Uploads are idempotent (stable event/snapshot ids) and bounded (recent + * records only); failures return [Result.retry] with scheduler backoff. + * No UI is touched — collection only. + */ +@HiltWorker +class AnalyticsSyncWorker @AssistedInject constructor( + @Assisted appContext: Context, + @Assisted params: WorkerParameters, + private val identityApi: IdentityApi, + private val syncApi: SyncApi, + private val preferencesDataStore: PreferencesDataStore, + private val cloudUsageMeter: CloudUsageMeter, + private val telemetryRepository: TelemetryRepository, +) : CoroutineWorker(appContext, params) { + + override suspend fun doWork(): Result { + if (!syncApi.isConfigured) return Result.success() + if (runCatching { FirebaseAuth.getInstance().currentUser == null }.getOrDefault(true)) { + return Result.success() + } + if (!preferencesDataStore.webConnected.first()) { + // Never synced before explicit Connect Web Dashboard approval. + return Result.success() + } + return try { + when (val me = identityApi.me()) { + is IdentityResult.Success -> { + if (!me.value.canSyncUsage()) { + preferencesDataStore.setWebConnection(false, null) + return Result.success() + } + } + is IdentityResult.Unauthorized -> return Result.success() + is IdentityResult.Failure -> return Result.retry() + } + syncOnce() + Result.success() + } catch (e: Exception) { + Timber.w(e, "[AnalyticsSync] run failed") + Result.retry() + } + } + + private suspend fun syncOnce() { + cloudUsageMeter.init() + val device = buildDevice() + + // ── Events: cloud records + local generations since the watermarks ── + val lastCloudId = preferencesDataStore.lastCloudRecordId.first() + val cloudRecords = cloudUsageMeter.snapshot().recentRecords + val newCloud = if (lastCloudId == null) cloudRecords + else cloudRecords.takeWhile { it.id != lastCloudId } + + val lastGenKey = preferencesDataStore.lastGenerationKey.first() + val generations = telemetryRepository.generationHistory.value + val newGenerations = if (lastGenKey == null) generations + else generations.takeLastWhile { generationKey(it) != lastGenKey } + + val events = (newCloud.map { it.toSyncEvent() } + newGenerations.map { it.toSyncEvent() }) + .take(SyncApi.MAX_BATCH_SIZE) + if (events.isNotEmpty()) { + when (val batch = syncApi.postBatch(EventsBatchRequest(device, events))) { + is IdentityResult.Success -> { + Timber.d("[AnalyticsSync] batch accepted=${batch.value.accepted} dup=${batch.value.duplicates}") + newCloud.firstOrNull()?.let { preferencesDataStore.setLastCloudRecordId(it.id) } + newGenerations.lastOrNull()?.let { preferencesDataStore.setLastGenerationKey(generationKey(it)) } + } + is IdentityResult.Unauthorized -> return + is IdentityResult.Failure -> { + if (batch.message == SyncApi.NOT_CONNECTED) { + preferencesDataStore.setWebConnection(false, null) + return + } + throw IllegalStateException("batch upload failed: ${batch.message}") + } + } + } + + // ── Snapshots: one hourly-bucketed rollup per surface ── + val nowMs = System.currentTimeMillis() + val lastBucket = preferencesDataStore.lastSnapshotBucket.first() + val cloudBucket = snapshotBucket(io.androllm.core.network.identity.SourcePage.CLOUD_USAGE_DASHBOARD, nowMs) + val devBucket = snapshotBucket(io.androllm.core.network.identity.SourcePage.DEVELOPER_PAGE, nowMs) + if (lastBucket != "$cloudBucket|$devBucket") { + val cloudSent = postSnapshot( + device = device, + sourcePage = io.androllm.core.network.identity.SourcePage.CLOUD_USAGE_DASHBOARD, + snapshotType = "dashboard_rollup", + snapshotId = "snap-$cloudBucket", + payload = buildCloudSnapshotPayload(cloudUsageMeter.snapshot()), + ) + val devSent = postSnapshot( + device = device, + sourcePage = io.androllm.core.network.identity.SourcePage.DEVELOPER_PAGE, + snapshotType = "telemetry_summary", + snapshotId = "snap-$devBucket", + payload = buildDeveloperSnapshotPayload( + generations = telemetryRepository.generationHistory.value, + deviceMetrics = telemetryRepository.deviceMetrics.value, + currentModel = telemetryRepository.currentModelName.value, + ), + ) + if (cloudSent && devSent) { + preferencesDataStore.setLastSnapshotBucket("$cloudBucket|$devBucket") + } + } + } + + private suspend fun postSnapshot( + device: SyncDevice, + sourcePage: String, + snapshotType: String, + snapshotId: String, + payload: JsonObject, + ): Boolean { + when (val res = syncApi.postSnapshot(SnapshotRequest(device, sourcePage, snapshotType, snapshotId, null, payload))) { + is IdentityResult.Success -> return true + is IdentityResult.Unauthorized -> return false + is IdentityResult.Failure -> { + if (res.message == SyncApi.NOT_CONNECTED) { + preferencesDataStore.setWebConnection(false, null) + return false + } + throw IllegalStateException("snapshot upload failed: ${res.message}") + } + } + } + + private fun buildDevice(): SyncDevice { + val androidId = runCatching { + Settings.Secure.getString(applicationContext.contentResolver, Settings.Secure.ANDROID_ID) + }.getOrNull().takeIf { !it.isNullOrBlank() } ?: "unknown" + return SyncDevice( + deviceIdentifier = "android:$androidId", + deviceName = "${Build.MANUFACTURER} ${Build.MODEL}".trim(), + platform = "android", + appVersion = BuildConfig.VERSION_NAME, + ) + } +} + +/** + * Opportunistic schedule: every 6h on any network, exponential backoff. + * Enqueue is idempotent (KEEP) and cheap — call at app start. The worker + * itself no-ops unless the user is signed in AND web-connected. + */ +@Singleton +class AnalyticsSyncScheduler @Inject constructor( + @ApplicationContext private val context: Context, +) { + fun schedule() { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) + .build() + val periodic = PeriodicWorkRequestBuilder(6, TimeUnit.HOURS) + .setConstraints(constraints) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.MINUTES) + .build() + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + UNIQUE_WORK, + ExistingPeriodicWorkPolicy.KEEP, + periodic, + ) + } + + companion object { + const val UNIQUE_WORK = "analytics_sync" + } +} diff --git a/app/src/main/java/io/androllm/app/sync/AnalyticsSyncTrigger.kt b/app/src/main/java/io/androllm/app/sync/AnalyticsSyncTrigger.kt new file mode 100644 index 00000000..72214cc0 --- /dev/null +++ b/app/src/main/java/io/androllm/app/sync/AnalyticsSyncTrigger.kt @@ -0,0 +1,90 @@ +package io.androllm.app.sync + +import androidx.work.Constraints +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import io.androllm.core.cloud.usage.CloudUsageMeter +import io.androllm.core.telemetry.TelemetryRepository +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import timber.log.Timber +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext + +/** Hilt access to WorkManager for the analytics sync trigger. */ +@Module +@InstallIn(SingletonComponent::class) +object AnalyticsSyncModule { + + @Provides + @Singleton + fun provideWorkManager(@ApplicationContext context: Context): WorkManager = + WorkManager.getInstance(context) +} + +/** + * Near-real-time sync trigger: every recorded cloud request and every + * completed local generation schedules an upload ~[debounceMs] later, so + * usage lands in Supabase within ~10 seconds of happening. + * + * Bursts coalesce — each new record restarts the timer, and WorkManager's + * REPLACE policy collapses overlapping requests into one run. The worker + * itself still gates on signed-in + web-connected and dedupes server-side, + * so triggers are always safe (and cheap no-ops when offline/guest). + */ +@Singleton +class AnalyticsSyncTrigger @Inject constructor( + private val workManager: WorkManager, + private val cloudUsageMeter: CloudUsageMeter, + private val telemetryRepository: TelemetryRepository, +) { + /** Overridable in tests (keep production at 8s so end-to-end stays <10s). */ + var debounceMs: Long = DEBOUNCE_MS + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private var pending: Job? = null + + /** Hook the existing dashboard data sources. Idempotent. */ + fun bind() { + cloudUsageMeter.onRecorded = { requestSoon() } + telemetryRepository.onGenerationRecorded = { requestSoon() } + } + + /** Schedule an upload [debounceMs] from now, cancelling any pending one. */ + fun requestSoon() { + pending?.cancel() + pending = scope.launch { + delay(debounceMs) + runCatching { enqueue() }.onFailure { + Timber.w(it, "[AnalyticsSync] trigger enqueue failed") + } + } + } + + private fun enqueue() { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + val once = OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .build() + workManager.enqueueUniqueWork(UNIQUE_SOON, ExistingWorkPolicy.REPLACE, once) + } + + companion object { + const val UNIQUE_SOON = "analytics_sync_soon" + const val DEBOUNCE_MS = 8_000L + } +} diff --git a/app/src/test/java/io/androllm/app/sync/AnalyticsMapperTest.kt b/app/src/test/java/io/androllm/app/sync/AnalyticsMapperTest.kt new file mode 100644 index 00000000..84c8217a --- /dev/null +++ b/app/src/test/java/io/androllm/app/sync/AnalyticsMapperTest.kt @@ -0,0 +1,157 @@ +package io.androllm.app.sync + +import io.androllm.core.cloud.usage.CloudErrorKind +import io.androllm.core.cloud.usage.CloudRequestKind +import io.androllm.core.cloud.usage.CloudUsageRecord +import io.androllm.core.cloud.usage.CloudUsageSnapshot +import io.androllm.core.cloud.usage.CloudUsageTotals +import io.androllm.core.network.identity.SourcePage +import io.androllm.core.telemetry.DeviceMetrics +import io.androllm.core.telemetry.GenerationStat +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Phase 3 (revised) — mapping rules from the existing in-app dashboards to + * sync payloads. Guards the dedup keys, the local/chat counting contract, + * and the no-PII shape of page snapshots. + */ +class AnalyticsMapperTest { + + private fun cloudRecord() = CloudUsageRecord( + id = "rec-uuid-1", + timestampMs = 1_720_000_000_000, + providerId = "openai", + providerName = "OpenAI", + modelId = "gpt-4o", + kind = CloudRequestKind.CHAT, + inputTokens = 100, + outputTokens = 200, + latencyMs = 2500, + firstTokenMs = 800, + ) + + @Test + fun `cloud record keeps stable id and maps cloud fields`() { + val event = cloudRecord().toSyncEvent() + assertEquals("rec-uuid-1", event.eventId) + assertEquals("chat_completed", event.eventType) + assertEquals(SourcePage.CLOUD_USAGE_DASHBOARD, event.sourcePage) + assertEquals("cloud", event.engineType) + assertEquals(300, event.totalTokens) + assertEquals(800L, event.timeToFirstTokenMs) + assertTrue(event.success) + assertNull(event.errorCode) + assertNull(event.sessionId) // Local ids must never ride along. + } + + @Test + fun `cloud failures preserve normalized error kind`() { + val event = cloudRecord().copy( + success = false, + errorKind = CloudErrorKind.RATE_LIMIT, + errorMessage = "slow down", + ).toSyncEvent() + assertFalse(event.success) + assertEquals("RATE_LIMIT", event.errorCode) + assertEquals("slow down", event.errorMessage) + } + + @Test + fun `cloud token overflow coerces instead of crashing`() { + val event = cloudRecord().copy(inputTokens = Long.MAX_VALUE).toSyncEvent() + assertEquals(Int.MAX_VALUE, event.tokensInput) + } + + @Test + fun `generation with output counts as local chat success`() { + val stat = GenerationStat( + timestampMs = 1_720_000_000_000, + promptTokens = 120, + generatedTokens = 340, + tokensPerSecond = 22.5f, + totalTimeMs = 2500, + firstTokenMs = 800, + stopReason = "stop", + modelName = "qwen3-0.6b", + ) + val event = stat.toSyncEvent() + assertEquals("chat_completed", event.eventType) + assertEquals("local", event.engineType) + assertEquals(SourcePage.DEVELOPER_PAGE, event.sourcePage) + assertTrue(event.success) + assertEquals(generationKey(stat), event.eventId) + assertEquals(generationKey(stat), generationKey(stat.copy())) + } + + @Test + fun `generation without output counts as failure with reason`() { + val stat = GenerationStat( + timestampMs = 1_720_000_000_001, + promptTokens = 120, + generatedTokens = 0, + tokensPerSecond = 0f, + totalTimeMs = 5000, + firstTokenMs = 0, + stopReason = "context_full", + modelName = "qwen3-0.6b", + ) + val event = stat.toSyncEvent() + assertFalse(event.success) + assertEquals("context_full", event.errorCode) + } + + @Test + fun `snapshot buckets are hourly and stable within the hour`() { + val hourMs = 3_600_000L + val base = 1_720_000_000_000L / hourMs * hourMs + assertEquals( + snapshotBucket(SourcePage.CLOUD_USAGE_DASHBOARD, base), + snapshotBucket(SourcePage.CLOUD_USAGE_DASHBOARD, base + 59_000), + ) + assertTrue( + snapshotBucket(SourcePage.CLOUD_USAGE_DASHBOARD, base) != + snapshotBucket(SourcePage.CLOUD_USAGE_DASHBOARD, base + hourMs) + ) + assertTrue( + snapshotBucket(SourcePage.CLOUD_USAGE_DASHBOARD, base) != + snapshotBucket(SourcePage.DEVELOPER_PAGE, base) + ) + } + + @Test + fun `cloud snapshot payload carries rollups without raw history`() { + val payload = buildCloudSnapshotPayload( + CloudUsageSnapshot( + generatedAtMs = 1, + total = CloudUsageTotals(requests = 10, totalTokens = 3000), + today = CloudUsageTotals(requests = 3, totalTokens = 900), + ) + ) + val encoded = payload.toString() + assertTrue(encoded.contains("3000")) + assertTrue(!encoded.contains("recentRecords")) + } + + @Test + fun `developer snapshot payload summarizes generations and device`() { + val payload = buildDeveloperSnapshotPayload( + generations = listOf( + GenerationStat(1, 100, 200, 20f, 1000, 400, "stop", "qwen3-0.6b"), + GenerationStat(2, 100, 200, 30f, 1000, 400, "stop", "qwen3-0.6b"), + ), + deviceMetrics = DeviceMetrics( + totalRamMb = 8000, availableRamMb = 4000, cpuCores = 8, + deviceModel = "Test Device", androidVersion = "14", + isVulkanSupported = false, totalStorageBytes = 1, usedStorageBytes = 0, + ), + currentModel = "qwen3-0.6b", + ) + val encoded = payload.toString() + assertTrue(encoded.contains("Test Device")) + assertTrue(encoded.contains("qwen3-0.6b")) + } +} diff --git a/app/src/test/java/io/androllm/app/sync/AnalyticsSyncTriggerTest.kt b/app/src/test/java/io/androllm/app/sync/AnalyticsSyncTriggerTest.kt new file mode 100644 index 00000000..93d99f37 --- /dev/null +++ b/app/src/test/java/io/androllm/app/sync/AnalyticsSyncTriggerTest.kt @@ -0,0 +1,78 @@ +package io.androllm.app.sync + +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import io.androllm.core.cloud.usage.CloudUsageMeter +import io.androllm.core.telemetry.TelemetryRepository +import io.mockk.mockk +import io.mockk.verify +import org.junit.Before +import org.junit.Test + +/** + * Sub-10s trigger contract: one record → one debounced upload; bursts + * coalesce into a single enqueue; nothing fires before the debounce elapses. + * Uses short real delays (no Robolectric needed — WorkManager is mocked). + */ +class AnalyticsSyncTriggerTest { + + private val workManager = mockk(relaxed = true) + private lateinit var trigger: AnalyticsSyncTrigger + + @Before + fun setUp() { + trigger = AnalyticsSyncTrigger( + workManager = workManager, + cloudUsageMeter = mockk(relaxed = true), + telemetryRepository = mockk(relaxed = true), + ) + trigger.debounceMs = 100 + } + + @Test + fun `single record enqueues upload after debounce`() { + trigger.requestSoon() + Thread.sleep(400) + + verify(exactly = 1) { + workManager.enqueueUniqueWork( + AnalyticsSyncTrigger.UNIQUE_SOON, + ExistingWorkPolicy.REPLACE, + any(), + ) + } + } + + @Test + fun `burst coalesces into a single enqueue`() { + repeat(5) { + trigger.requestSoon() + Thread.sleep(40) + } + Thread.sleep(400) + + verify(exactly = 1) { + workManager.enqueueUniqueWork( + AnalyticsSyncTrigger.UNIQUE_SOON, + ExistingWorkPolicy.REPLACE, + any(), + ) + } + } + + @Test + fun `nothing enqueued before debounce elapses`() { + trigger.requestSoon() + Thread.sleep(30) + + verify(exactly = 0) { + workManager.enqueueUniqueWork(any(), any(), any()) + } + + Thread.sleep(400) + verify(exactly = 1) { + workManager.enqueueUniqueWork(any(), any(), any()) + } + } +} diff --git a/core/cloud/src/main/java/io/androllm/core/cloud/usage/CloudUsageMeter.kt b/core/cloud/src/main/java/io/androllm/core/cloud/usage/CloudUsageMeter.kt index d9d53972..9a6f7364 100644 --- a/core/cloud/src/main/java/io/androllm/core/cloud/usage/CloudUsageMeter.kt +++ b/core/cloud/src/main/java/io/androllm/core/cloud/usage/CloudUsageMeter.kt @@ -68,6 +68,14 @@ class CloudUsageMeter( private var saveJob: Job? = null private var dirty = false + /** + * Fired (best-effort, never throwing) after every successfully recorded + * request. The app's sync layer hooks this to enqueue a debounced upload + * so cloud usage reaches the backend within seconds — the dashboard reads + * [snapshots], which is untouched by this callback. + */ + var onRecorded: (() -> Unit)? = null + /** Loads persisted state and publishes the first snapshot. Idempotent. */ suspend fun init() { if (loaded) return @@ -111,6 +119,7 @@ class CloudUsageMeter( }.onFailure { e -> Timber.w(e, "CloudUsageMeter: record failed — usage counters may lag") } + runCatching { onRecorded?.invoke() } } /** Convenience factory: builds a record with cost already estimated. */ diff --git a/core/cloud/src/test/java/io/androllm/core/cloud/usage/CloudUsageMeterListenerTest.kt b/core/cloud/src/test/java/io/androllm/core/cloud/usage/CloudUsageMeterListenerTest.kt new file mode 100644 index 00000000..4255e891 --- /dev/null +++ b/core/cloud/src/test/java/io/androllm/core/cloud/usage/CloudUsageMeterListenerTest.kt @@ -0,0 +1,58 @@ +package io.androllm.core.cloud.usage + +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * The sync trigger hook: every recorded request must fire [CloudUsageMeter.onRecorded] + * without ever breaking recording itself. + */ +class CloudUsageMeterListenerTest { + + private fun meter(): CloudUsageMeter = + CloudUsageMeter(InMemoryCloudUsageStore(), persistDebounceMs = 0) + + private fun record(meter: CloudUsageMeter) { + meter.record( + meter.buildRecord( + providerId = "openai", + providerName = "OpenAI", + modelId = "gpt-4o", + kind = CloudRequestKind.CHAT, + inputTokens = 10, + outputTokens = 20, + ) + ) + } + + @Test + fun `record fires onRecorded`() { + val meter = meter() + val latch = CountDownLatch(1) + meter.onRecorded = { latch.countDown() } + record(meter) + assertTrue(latch.await(5, TimeUnit.SECONDS)) + assertEquals(1, meter.snapshot().recentRecords.size) + } + + @Test + fun `throwing listener never breaks recording`() { + val meter = meter() + meter.onRecorded = { throw IllegalStateException("boom") } + record(meter) + assertEquals(1, meter.snapshot().recentRecords.size) + } + + @Test + fun `burst of records fires listener per record`() { + val meter = meter() + val count = AtomicInteger(0) + meter.onRecorded = { count.incrementAndGet() } + repeat(5) { record(meter) } + assertEquals(5, count.get()) + } +} diff --git a/core/datastore/src/main/java/io/androllm/core/datastore/PreferencesDataStore.kt b/core/datastore/src/main/java/io/androllm/core/datastore/PreferencesDataStore.kt index 0da308e4..1fbca65c 100644 --- a/core/datastore/src/main/java/io/androllm/core/datastore/PreferencesDataStore.kt +++ b/core/datastore/src/main/java/io/androllm/core/datastore/PreferencesDataStore.kt @@ -57,6 +57,16 @@ class PreferencesDataStore @Inject constructor( val UI_DENSITY = stringPreferencesKey("ui_density") val CHAT_WALLPAPER = stringPreferencesKey("chat_wallpaper") val REDUCE_MOTION = booleanPreferencesKey("reduce_motion") + // Phase 2 — web dashboard connector. Display cache of the + // backend-driven `web_connected` state (source of truth stays + // server-side on the verified Firebase UID). Never gates sync. + val WEB_CONNECTED = booleanPreferencesKey("web_connected") + val WEB_CONNECTED_AT = stringPreferencesKey("web_connected_at") + // Phase 3 — analytics sync watermarks (skip already-uploaded windows; + // server-side event_id/snapshot_id dedup is the real safety net). + val SYNC_LAST_CLOUD_RECORD_ID = stringPreferencesKey("sync_last_cloud_record_id") + val SYNC_LAST_GENERATION_KEY = stringPreferencesKey("sync_last_generation_key") + val SYNC_LAST_SNAPSHOT_BUCKET = stringPreferencesKey("sync_last_snapshot_bucket") // Prompt Studio settings val STUDIO_DEFAULT_TEMPLATE = stringPreferencesKey("studio_default_template") val STUDIO_AUTO_PREVIEW = booleanPreferencesKey("studio_auto_preview") @@ -472,4 +482,64 @@ class PreferencesDataStore @Inject constructor( * Returns whether this is the first launch. */ suspend fun isFirstLaunch(): Boolean = firstLaunch.first() + + /** + * Phase 2 — last-known web-dashboard connection flag (display cache). + * The backend profile (`GET /me`) is the source of truth; this only lets + * the connector card render instantly/offline. Defaults to false so a + * fresh install can never look connected before explicit approval. + */ + val webConnected: Flow = dataStore.data.map { preferences -> + preferences[Keys.WEB_CONNECTED] ?: false + } + + /** Phase 2 — ISO-8601 timestamp of the last-known connection, if any. */ + val webConnectedAt: Flow = dataStore.data.map { preferences -> + preferences[Keys.WEB_CONNECTED_AT] + } + + /** + * Phase 2 — persists the last-known connection state after a backend + * round-trip (`POST /auth/connect`, `DELETE /auth/connect`, `GET /me`). + * Pass `connectedAt = null` when disconnecting. + */ + suspend fun setWebConnection(connected: Boolean, connectedAt: String?) { + dataStore.edit { preferences -> + preferences[Keys.WEB_CONNECTED] = connected + if (connectedAt != null) { + preferences[Keys.WEB_CONNECTED_AT] = connectedAt + } else { + preferences.remove(Keys.WEB_CONNECTED_AT) + } + } + } + + /** + * Phase 3 — analytics sync watermarks. Each records the newest item already + * handed to the backend so the next run uploads only what is new. Retries + * reuse the same stable ids, so server-side dedup catches anything sent twice. + */ + val lastCloudRecordId: Flow = dataStore.data.map { preferences -> + preferences[Keys.SYNC_LAST_CLOUD_RECORD_ID] + } + + val lastGenerationKey: Flow = dataStore.data.map { preferences -> + preferences[Keys.SYNC_LAST_GENERATION_KEY] + } + + val lastSnapshotBucket: Flow = dataStore.data.map { preferences -> + preferences[Keys.SYNC_LAST_SNAPSHOT_BUCKET] + } + + suspend fun setLastCloudRecordId(id: String) { + dataStore.edit { preferences -> preferences[Keys.SYNC_LAST_CLOUD_RECORD_ID] = id } + } + + suspend fun setLastGenerationKey(key: String) { + dataStore.edit { preferences -> preferences[Keys.SYNC_LAST_GENERATION_KEY] = key } + } + + suspend fun setLastSnapshotBucket(bucket: String) { + dataStore.edit { preferences -> preferences[Keys.SYNC_LAST_SNAPSHOT_BUCKET] = bucket } + } } diff --git a/core/network/src/main/java/io/androllm/core/network/identity/IdentityApi.kt b/core/network/src/main/java/io/androllm/core/network/identity/IdentityApi.kt new file mode 100644 index 00000000..0793de39 --- /dev/null +++ b/core/network/src/main/java/io/androllm/core/network/identity/IdentityApi.kt @@ -0,0 +1,167 @@ +package io.androllm.core.network.identity + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.bearerAuth +import io.ktor.client.request.delete +import io.ktor.client.request.get +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.request.url +import io.ktor.http.ContentType +import io.ktor.http.contentType +import io.ktor.http.isSuccess +import timber.log.Timber + +/** + * Phase 1 identity API — thin Ktor wrapper over the private backend. + * + * - Auth is always `Authorization: Bearer `. + * - [idTokenProvider] is injected so this module stays Firebase-free and testable. + * - Identity comes from the verified token server-side; request bodies carry + * display hints only (never a user id). + * + * Endpoints: POST /auth/verify, GET /auth/session, GET /me. + * Phase 2 adds: POST /auth/connect, DELETE /auth/connect (explicit + * account-linking consent; the private backend flips `web_connected`). + * Still no analytics/sync methods here — Phase 3 owns event ingestion, and it + * must only run for profiles where [UserProfile.canSyncUsage] is true. + */ +class IdentityApi( + private val baseUrl: String, + private val httpClient: HttpClient, + private val idTokenProvider: suspend (forceRefresh: Boolean) -> String?, + /** False when no backend URL is configured — every call short-circuits. */ + val isConfigured: Boolean = true, +) { + + sealed interface IdentityResult { + data class Success(val value: T) : IdentityResult + data class Unauthorized(val message: String = "unauthorized") : IdentityResult + data class Failure(val message: String, val cause: Throwable? = null) : IdentityResult + } + + private fun trimmedBase(): String = baseUrl.trim().trimEnd('/') + + private suspend fun tokenOrUnauthorized(): String? { + val token = runCatching { idTokenProvider(false) }.getOrNull() + if (token.isNullOrBlank()) { + Timber.w("[Identity] no Firebase ID token (signed out?)") + } + return token?.takeIf { it.isNotBlank() } + } + + /** Verify session with backend; upserts the profile server-side. */ + suspend fun verify( + displayName: String? = null, + photoUrl: String? = null, + appVersion: String? = null, + ): IdentityResult { + val token = tokenOrUnauthorized() ?: return IdentityResult.Unauthorized("no id token") + return try { + val response = httpClient.post { + url("${trimmedBase()}/auth/verify") + bearerAuth(token) + contentType(ContentType.Application.Json) + setBody(AuthVerifyRequest(displayName, photoUrl, DeviceInfo(appVersion = appVersion))) + } + when { + response.status.isSuccess() -> IdentityResult.Success(response.body()) + response.status.value == 401 -> IdentityResult.Unauthorized() + else -> IdentityResult.Failure("verify failed: HTTP ${response.status.value}") + } + } catch (e: Exception) { + Timber.w(e, "[Identity] verify failed") + IdentityResult.Failure("verify failed: ${e.message}", e) + } + } + + /** Lightweight session check used on app startup. */ + suspend fun session(): IdentityResult { + val token = tokenOrUnauthorized() ?: return IdentityResult.Unauthorized("no id token") + return try { + val response = httpClient.get { + url("${trimmedBase()}/auth/session") + bearerAuth(token) + } + when { + response.status.isSuccess() -> IdentityResult.Success(response.body()) + response.status.value == 401 -> IdentityResult.Unauthorized() + else -> IdentityResult.Failure("session failed: HTTP ${response.status.value}") + } + } catch (e: Exception) { + Timber.w(e, "[Identity] session failed") + IdentityResult.Failure("session failed: ${e.message}", e) + } + } + + /** Fetch the current backend profile for the verified UID. */ + suspend fun me(): IdentityResult { + val token = tokenOrUnauthorized() ?: return IdentityResult.Unauthorized("no id token") + return try { + val response = httpClient.get { + url("${trimmedBase()}/me") + bearerAuth(token) + } + when { + response.status.isSuccess() -> IdentityResult.Success(response.body()) + response.status.value == 401 -> IdentityResult.Unauthorized() + else -> IdentityResult.Failure("me failed: HTTP ${response.status.value}") + } + } catch (e: Exception) { + Timber.w(e, "[Identity] me failed") + IdentityResult.Failure("me failed: ${e.message}", e) + } + } + + /** + * Phase 2 — explicit account linking. Call ONLY after the user taps + * "Connect Web Dashboard" and confirms. The private backend verifies the + * ID token, sets `web_connected = true` / `web_connected_at = now()` for + * the verified UID (idempotent), and returns the updated profile. + * No usage data is uploaded by this call. + */ + suspend fun connect(): IdentityResult { + if (!isConfigured) return IdentityResult.Failure("backend not configured") + val token = tokenOrUnauthorized() ?: return IdentityResult.Unauthorized("no id token") + return try { + val response = httpClient.post { + url("${trimmedBase()}/auth/connect") + bearerAuth(token) + contentType(ContentType.Application.Json) + setBody("{}") + } + when { + response.status.isSuccess() -> IdentityResult.Success(response.body()) + response.status.value == 401 -> IdentityResult.Unauthorized() + else -> IdentityResult.Failure("connect failed: HTTP ${response.status.value}") + } + } catch (e: Exception) { + Timber.w(e, "[Identity] connect failed") + IdentityResult.Failure("connect failed: ${e.message}", e) + } + } + + /** + * Phase 2 — revoke account linking. Idempotent; clears `web_connected` + * for the verified UID and returns the updated profile. + */ + suspend fun disconnect(): IdentityResult { + if (!isConfigured) return IdentityResult.Failure("backend not configured") + val token = tokenOrUnauthorized() ?: return IdentityResult.Unauthorized("no id token") + return try { + val response = httpClient.delete { + url("${trimmedBase()}/auth/connect") + bearerAuth(token) + } + when { + response.status.isSuccess() -> IdentityResult.Success(response.body()) + response.status.value == 401 -> IdentityResult.Unauthorized() + else -> IdentityResult.Failure("disconnect failed: HTTP ${response.status.value}") + } + } catch (e: Exception) { + Timber.w(e, "[Identity] disconnect failed") + IdentityResult.Failure("disconnect failed: ${e.message}", e) + } + } +} diff --git a/core/network/src/main/java/io/androllm/core/network/identity/SyncApi.kt b/core/network/src/main/java/io/androllm/core/network/identity/SyncApi.kt new file mode 100644 index 00000000..dcd55f3a --- /dev/null +++ b/core/network/src/main/java/io/androllm/core/network/identity/SyncApi.kt @@ -0,0 +1,168 @@ +package io.androllm.core.network.identity + +import io.androllm.core.network.identity.IdentityApi.IdentityResult +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.bearerAuth +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.request.url +import io.ktor.http.ContentType +import io.ktor.http.contentType +import io.ktor.http.isSuccess +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject +import timber.log.Timber + +/** In-app surfaces that may produce synced analytics. */ +object SourcePage { + const val CLOUD_USAGE_DASHBOARD = "cloud_usage_dashboard" + const val DEVELOPER_PAGE = "developer_page" +} + +@Serializable +data class SyncDevice( + @SerialName("device_identifier") val deviceIdentifier: String, + @SerialName("device_name") val deviceName: String? = null, + val platform: String = "android", + @SerialName("app_version") val appVersion: String? = null, + val metadata: JsonObject = JsonObject(emptyMap()), +) + +@Serializable +data class SyncEvent( + @SerialName("event_id") val eventId: String, + @SerialName("event_type") val eventType: String, + @SerialName("session_id") val sessionId: String? = null, + @SerialName("source_page") val sourcePage: String? = null, + @SerialName("model_name") val modelName: String? = null, + @SerialName("provider_name") val providerName: String? = null, + @SerialName("engine_type") val engineType: String? = null, + @SerialName("tokens_input") val tokensInput: Int = 0, + @SerialName("tokens_output") val tokensOutput: Int = 0, + @SerialName("total_tokens") val totalTokens: Int = 0, + @SerialName("latency_ms") val latencyMs: Long? = null, + @SerialName("time_to_first_token_ms") val timeToFirstTokenMs: Long? = null, + val success: Boolean = true, + @SerialName("error_code") val errorCode: String? = null, + @SerialName("error_message") val errorMessage: String? = null, + val metadata: JsonObject = JsonObject(emptyMap()), + @SerialName("created_at") val createdAt: String? = null, +) + +@Serializable +data class EventsBatchRequest( + val device: SyncDevice, + val events: List, +) + +@Serializable +data class BatchRejection( + val index: Int, + @SerialName("event_id") val eventId: String? = null, + val reason: String? = null, +) + +@Serializable +data class EventsBatchResponse( + val accepted: Int = 0, + val duplicates: Int = 0, + val rejected: List = emptyList(), + @SerialName("device_id") val deviceId: String? = null, +) + +@Serializable +data class SnapshotRequest( + val device: SyncDevice, + @SerialName("source_page") val sourcePage: String, + @SerialName("snapshot_type") val snapshotType: String, + @SerialName("snapshot_id") val snapshotId: String, + @SerialName("session_id") val sessionId: String? = null, + val payload: JsonObject, +) + +@Serializable +data class SnapshotResponse( + val stored: Boolean = false, + val duplicate: Boolean = false, + @SerialName("device_id") val deviceId: String? = null, +) + +/** + * Phase 3 (revised) — usage + snapshot ingestion client. + * + * Callers MUST gate on [UserProfile.canSyncUsage] (verified via [IdentityApi.me]) + * before invoking anything here: the backend rejects unconnected users with + * 403 `web_not_connected`, surfaced as [IdentityResult.Failure]. + * Retries are safe — [SyncEvent.eventId] and [SnapshotRequest.snapshotId] are + * stable dedup keys, so re-sending never double-stores. + */ +class SyncApi( + private val baseUrl: String, + private val httpClient: HttpClient, + private val idTokenProvider: suspend (forceRefresh: Boolean) -> String?, + /** False when no backend URL is configured — every call short-circuits. */ + val isConfigured: Boolean = true, +) { + + companion object { + const val NOT_CONNECTED = "web_not_connected" + const val MAX_BATCH_SIZE = 500 + } + + private fun trimmedBase(): String = baseUrl.trim().trimEnd('/') + + private suspend fun tokenOrUnauthorized(): String? { + val token = runCatching { idTokenProvider(false) }.getOrNull() + return token?.takeIf { it.isNotBlank() } + } + + /** Upload up to [MAX_BATCH_SIZE] events; partial success per item. */ + suspend fun postBatch(request: EventsBatchRequest): IdentityResult { + if (!isConfigured) return IdentityResult.Failure("backend not configured") + val token = tokenOrUnauthorized() ?: return IdentityResult.Unauthorized("no id token") + return try { + val response = httpClient.post { + url("${trimmedBase()}/events/batch") + bearerAuth(token) + contentType(ContentType.Application.Json) + setBody(request) + } + when (response.status.value) { + in 200..299 -> IdentityResult.Success(response.body()) + 401 -> IdentityResult.Unauthorized() + 403 -> IdentityResult.Failure(NOT_CONNECTED) + 400 -> IdentityResult.Failure("invalid payload") + else -> IdentityResult.Failure("batch failed: HTTP ${response.status.value}") + } + } catch (e: Exception) { + Timber.w(e, "[Sync] batch failed") + IdentityResult.Failure("batch failed: ${e.message}", e) + } + } + + /** Upload one page-level snapshot (cloud dashboard / developer page rollup). */ + suspend fun postSnapshot(request: SnapshotRequest): IdentityResult { + if (!isConfigured) return IdentityResult.Failure("backend not configured") + val token = tokenOrUnauthorized() ?: return IdentityResult.Unauthorized("no id token") + return try { + val response = httpClient.post { + url("${trimmedBase()}/analytics/snapshot") + bearerAuth(token) + contentType(ContentType.Application.Json) + setBody(request) + } + when (response.status.value) { + in 200..299 -> IdentityResult.Success(response.body()) + 401 -> IdentityResult.Unauthorized() + 403 -> IdentityResult.Failure(NOT_CONNECTED) + 400 -> IdentityResult.Failure("invalid payload") + else -> IdentityResult.Failure("snapshot failed: HTTP ${response.status.value}") + } + } catch (e: Exception) { + Timber.w(e, "[Sync] snapshot failed") + IdentityResult.Failure("snapshot failed: ${e.message}", e) + } + } +} diff --git a/core/network/src/main/java/io/androllm/core/network/identity/UserProfile.kt b/core/network/src/main/java/io/androllm/core/network/identity/UserProfile.kt new file mode 100644 index 00000000..13a4fa76 --- /dev/null +++ b/core/network/src/main/java/io/androllm/core/network/identity/UserProfile.kt @@ -0,0 +1,58 @@ +package io.androllm.core.network.identity + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject + +/** + * Phase 1 backend user profile. Mirrors the private backend `users` row. + * + * Identity key is [firebaseUid] (verified server-side). The app must never + * invent or forward a user id — the backend derives it from the ID token. + */ +@Serializable +data class UserProfile( + val id: String, + @SerialName("firebase_uid") val firebaseUid: String, + val email: String? = null, + @SerialName("display_name") val displayName: String? = null, + @SerialName("photo_url") val photoUrl: String? = null, + @SerialName("created_at") val createdAt: String, + @SerialName("last_seen_at") val lastSeenAt: String, + @SerialName("sync_enabled") val syncEnabled: Boolean = false, + val settings: JsonObject = JsonObject(emptyMap()), + // Phase 2 consent flags (always false in Phase 1; read-only here). + @SerialName("web_connected") val webConnected: Boolean = false, + @SerialName("web_connected_at") val webConnectedAt: String? = null, +) + +@Serializable +data class AuthVerifyRequest( + @SerialName("display_name") val displayName: String? = null, + @SerialName("photo_url") val photoUrl: String? = null, + val device: DeviceInfo? = null, +) + +@Serializable +data class DeviceInfo( + val platform: String = "android", + @SerialName("app_version") val appVersion: String? = null, +) + +@Serializable +data class AuthSessionResponse( + val authenticated: Boolean, + val user: UserProfile, +) + +/** Extra settings payload stays opaque — auth layer never interprets it. */ +typealias SettingsJson = Map + +/** + * Phase 2 sync gate. Phase 3 event ingestion must check this before uploading + * ANY usage data: only an explicitly connected account may sync. The flag is + * backend-driven (`web_connected` set via POST /auth/connect); the local + * DataStore copy is a display cache only and must never bypass this check. + */ +fun UserProfile.canSyncUsage(): Boolean = webConnected diff --git a/core/network/src/test/java/io/androllm/core/network/identity/SyncApiSerializationTest.kt b/core/network/src/test/java/io/androllm/core/network/identity/SyncApiSerializationTest.kt new file mode 100644 index 00000000..1ce42aad --- /dev/null +++ b/core/network/src/test/java/io/androllm/core/network/identity/SyncApiSerializationTest.kt @@ -0,0 +1,74 @@ +package io.androllm.core.network.identity + +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Phase 3 (revised) — sync payload contracts. The JSON keys here must match + * the private backend (POST /events/batch, POST /analytics/snapshot) exactly. + */ +class SyncApiSerializationTest { + + private val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; explicitNulls = false } + + @Test + fun `sync event round-trips with source page`() { + val event = SyncEvent( + eventId = "evt-1", + eventType = "chat_completed", + sourcePage = SourcePage.CLOUD_USAGE_DASHBOARD, + modelName = "gpt-4o", + providerName = "openai", + engineType = "cloud", + tokensInput = 10, + tokensOutput = 20, + totalTokens = 30, + latencyMs = 2500L, + timeToFirstTokenMs = 800L, + success = true, + createdAt = "2026-09-21T13:00:00Z", + ) + val decoded = json.decodeFromString(SyncEvent.serializer(), json.encodeToString(SyncEvent.serializer(), event)) + assertEquals(event, decoded) + } + + @Test + fun `batch request serializes device and events`() { + val body = EventsBatchRequest( + device = SyncDevice(deviceIdentifier = "android:abc", deviceName = "Pixel", appVersion = "1.1.6"), + events = listOf(SyncEvent(eventId = "e1", eventType = "chat_completed")), + ) + val encoded = json.encodeToString(EventsBatchRequest.serializer(), body) + assertTrue(encoded.contains("device_identifier")) + assertTrue(encoded.contains("chat_completed")) + // Never a user id — identity travels in the Bearer token. + assertTrue(!encoded.contains("firebase_uid") && !encoded.contains("user_id")) + } + + @Test + fun `snapshot request round-trips`() { + val payload: JsonObject = buildJsonObject { put("requests", 10) } + val req = SnapshotRequest( + device = SyncDevice(deviceIdentifier = "android:abc"), + sourcePage = SourcePage.DEVELOPER_PAGE, + snapshotType = "telemetry_summary", + snapshotId = "snap-developer_page:2026-09-21-14", + payload = payload, + ) + val decoded = json.decodeFromString(SnapshotRequest.serializer(), json.encodeToString(SnapshotRequest.serializer(), req)) + assertEquals(req, decoded) + } + + @Test + fun `batch response defaults tolerate empty backend replies`() { + val decoded = json.decodeFromString(EventsBatchResponse.serializer(), "{}") + assertEquals(0, decoded.accepted) + assertTrue(decoded.rejected.isEmpty()) + } +} diff --git a/core/network/src/test/java/io/androllm/core/network/identity/UserProfileSerializationTest.kt b/core/network/src/test/java/io/androllm/core/network/identity/UserProfileSerializationTest.kt new file mode 100644 index 00000000..fd1803db --- /dev/null +++ b/core/network/src/test/java/io/androllm/core/network/identity/UserProfileSerializationTest.kt @@ -0,0 +1,79 @@ +package io.androllm.core.network.identity + +import kotlinx.serialization.json.Json +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Phase 1 identity contract tests — UserProfile JSON must match the private + * backend (`POST /auth/verify`, `GET /me`) exactly. + * Phase 2 adds the sync-gate rule: nothing may sync before `web_connected`. + */ +class UserProfileSerializationTest { + + private val json = Json { ignoreUnknownKeys = true; encodeDefaults = true; explicitNulls = false } + + @Test + fun `user profile deserializes backend payload`() { + val payload = """ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "firebase_uid": "firebaseUid123", + "email": "user@example.com", + "display_name": "Ada", + "photo_url": "https://example.com/a.png", + "created_at": "2026-09-21T00:00:00.000Z", + "last_seen_at": "2026-09-21T00:00:00.000Z", + "sync_enabled": false, + "settings": {}, + "web_connected": false, + "web_connected_at": null + } + """.trimIndent() + val profile = json.decodeFromString(UserProfile.serializer(), payload) + assertEquals("firebaseUid123", profile.firebaseUid) + assertEquals("user@example.com", profile.email) + assertFalse(profile.syncEnabled) + assertFalse(profile.webConnected) + } + + @Test + fun `verify request omits null hints`() { + val encoded = json.encodeToString( + AuthVerifyRequest.serializer(), + AuthVerifyRequest(displayName = null, photoUrl = null, device = DeviceInfo(platform = "android")), + ) + // No user id may ever be sent — identity comes from the Bearer token. + assertFalse(encoded.contains("firebase_uid")) + assertFalse(encoded.contains("display_name")) + } + + @Test + fun `sync gate denies unconnected profiles`() { + val payload = """ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "firebase_uid": "firebaseUid123", + "email": "user@example.com", + "display_name": "Ada", + "photo_url": null, + "created_at": "2026-09-21T00:00:00.000Z", + "last_seen_at": "2026-09-21T00:00:00.000Z", + "sync_enabled": false, + "settings": {}, + "web_connected": false, + "web_connected_at": null + } + """.trimIndent() + val unconnected = json.decodeFromString(UserProfile.serializer(), payload) + assertFalse(unconnected.canSyncUsage()) + + val connected = unconnected.copy( + webConnected = true, + webConnectedAt = "2026-09-22T00:00:00.000Z", + ) + assertTrue(connected.canSyncUsage()) + } +} diff --git a/core/telemetry/src/main/java/io/androllm/core/telemetry/TelemetryRepository.kt b/core/telemetry/src/main/java/io/androllm/core/telemetry/TelemetryRepository.kt index 6b64471e..6cfbecad 100644 --- a/core/telemetry/src/main/java/io/androllm/core/telemetry/TelemetryRepository.kt +++ b/core/telemetry/src/main/java/io/androllm/core/telemetry/TelemetryRepository.kt @@ -75,6 +75,13 @@ class TelemetryRepository @Inject constructor( private var tickCount = 0 private var lastStorage: io.androllm.core.utils.StorageStats? = null + /** + * Fired (best-effort, never throwing) after every completed generation is + * recorded. The app's sync layer hooks this to enqueue a debounced upload + * so local generations reach the backend within seconds. + */ + var onGenerationRecorded: (() -> Unit)? = null + init { // Track the loaded model name for generation history entries. scope.launch { @@ -117,6 +124,7 @@ class TelemetryRepository @Inject constructor( max = MAX_GENERATION_STATS ) } + runCatching { onGenerationRecorded?.invoke() } } /** diff --git a/core/telemetry/src/test/java/io/androllm/core/telemetry/TelemetryListenerTest.kt b/core/telemetry/src/test/java/io/androllm/core/telemetry/TelemetryListenerTest.kt new file mode 100644 index 00000000..a8c5d8a7 --- /dev/null +++ b/core/telemetry/src/test/java/io/androllm/core/telemetry/TelemetryListenerTest.kt @@ -0,0 +1,92 @@ +package io.androllm.core.telemetry + +import io.androllm.core.utils.DeviceHardwareInfo +import io.androllm.core.utils.DeviceInfoCollector +import io.androllm.core.utils.StorageStats +import io.androllm.core.utils.StorageUtils +import io.androllm.engine.api.EngineRepository +import io.androllm.engine.api.EngineState +import io.androllm.engine.api.GenerationState +import io.androllm.engine.models.EngineStats +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkAll +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +/** + * The sync trigger hook: every completed generation must fire + * [TelemetryRepository.onGenerationRecorded] without ever breaking recording. + */ +class TelemetryListenerTest { + + @Before + fun setUp() { + mockkObject(DeviceInfoCollector, StorageUtils) + every { DeviceInfoCollector.collectDeviceInfo(any()) } returns DeviceHardwareInfo( + deviceName = "Test", + manufacturer = "Test", + androidVersion = "14", + apiLevel = 34, + abi = "arm64-v8a", + cpuCores = 8, + totalRamBytes = 8L * 1024 * 1024 * 1024, + availableRamBytes = 4L * 1024 * 1024 * 1024, + totalRamGb = 8f, + freeStorageBytes = 0L, + isVulkanSupported = false, + ) + every { StorageUtils.getStorageStats(any()) } returns StorageStats( + totalBytes = 100L, + usedBytes = 40L, + freeBytes = 60L, + ) + } + + @After + fun tearDown() { + unmockkAll() + } + + private fun repository(performanceStats: MutableStateFlow): TelemetryRepository { + val engine = mockk { + every { engineState } returns MutableStateFlow(EngineState.Unloaded) + every { generationState } returns MutableStateFlow(GenerationState.Idle) + every { memoryStats } returns MutableStateFlow(null) + every { this@mockk.performanceStats } returns performanceStats + } + return TelemetryRepository(mockk(), engine) + } + + @Test + fun `completed generation fires onGenerationRecorded`() { + val performanceStats = MutableStateFlow(null) + val repo = repository(performanceStats) + val latch = CountDownLatch(1) + repo.onGenerationRecorded = { latch.countDown() } + + performanceStats.value = EngineStats(promptTokens = 10, generatedTokens = 20) + + assertTrue(latch.await(5, TimeUnit.SECONDS)) + } + + @Test + fun `empty stats never fire the listener`() { + val performanceStats = MutableStateFlow(null) + val repo = repository(performanceStats) + var calls = 0 + repo.onGenerationRecorded = { calls++ } + + performanceStats.value = EngineStats(promptTokens = 0, generatedTokens = 0) + Thread.sleep(300) + + // Zero-token stats are filtered before recording (and before the callback). + assertTrue(calls == 0) + } +} diff --git a/documentation/backend/identity-foundation.md b/documentation/backend/identity-foundation.md new file mode 100644 index 00000000..138c0ea5 --- /dev/null +++ b/documentation/backend/identity-foundation.md @@ -0,0 +1,225 @@ +# AndroLLM Identity Foundation (Phase 1) + +Shared identity layer for the Android app and the web dashboard. + +- Firebase Authentication is the **only** login system (app + web share the same Firebase project). +- The private backend verifies Firebase ID tokens with the **Firebase Admin SDK** (server-side only). +- The backend owns a stable user profile keyed by `firebase_uid`. +- The Firebase UID is the canonical identity key. **Never trust client-supplied user IDs.** + +Phase 1 scope: identity only. No analytics, no sync, no charts. + +--- + +## 1. Flow + +``` +Android app Private backend Website +─────────── ─────────────── ─────── +Firebase sign-in (Google/GitHub) + └─> FirebaseUser (uid, email, + displayName, photoUrl) + │ + ├── getIdToken() ──> Authorization: Bearer + │ │ + │ ├── POST /auth/verify ──> verifyIdToken() + │ │ upsert users by firebase_uid + │ │ update last_seen_at + │ │ return UserProfile + │ │ + │ ├── GET /auth/session ──> verify + return profile + │ └── GET /me ──> verify + return profile + │ + └── (Phase 2+) explicit "Connect Web Dashboard" + consent stored server-side before ANY usage sync + +Website (same Firebase project): + Firebase web sign-in (Google/GitHub, same project) + └─> getIdToken() ──> same 3 endpoints ──> same UserProfile +``` + +Both clients attach the token the same way: + +``` +Authorization: Bearer +``` + +--- + +## 2. Endpoints (private backend) + +Base URL is environment-configured, never hard-coded: + +- Android: `BuildConfig.BACKEND_BASE_URL` (or DataStore remote config) +- Web: `NEXT_PUBLIC_BACKEND_URL` + +### POST /auth/verify + +Verify the caller's ID token and create/update the user profile. + +Request: + +```http +POST /auth/verify HTTP/1.1 +Authorization: Bearer +Content-Type: application/json + +{ + "display_name": "Optional client hint (ignored for identity)", + "photo_url": "Optional client hint (ignored for identity)", + "device": { "platform": "android|web", "app_version": "1.1.6" } +} +``` + +Server behavior: + +1. Extract Bearer token. 401 if missing/malformed. +2. `admin.auth().verifyIdToken(idToken)` — 401 if invalid/expired/revoked. +3. Take `uid`, `email`, `name`, `picture` **from the verified token**, not the body. +4. `INSERT ... ON CONFLICT (firebase_uid) DO UPDATE`: + - set `email`, `display_name`, `photo_url` from token (fallback to body hints only if token lacks them) + - set `last_seen_at = now()` + - never overwrite `id`, `firebase_uid`, `created_at`, `web_connected`, `web_connected_at` +5. Return the profile. + +Response `200`: + +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "firebase_uid": "firebaseUid123", + "email": "user@example.com", + "display_name": "Ada", + "photo_url": "https://...", + "created_at": "2026-09-21T00:00:00.000Z", + "last_seen_at": "2026-09-21T00:00:00.000Z", + "sync_enabled": false, + "settings": {}, + "web_connected": false, + "web_connected_at": null +} +``` + +Errors: `401 { "error": "unauthorized" }`, `503 { "error": "auth_unavailable" }`. + +### GET /auth/session + +Lightweight session check. Same auth header, no body. +Returns `{ "authenticated": true, "user": }` or `401`. + +Used by both clients on startup to decide: guest vs signed-in vs backend-linked. + +### GET /me + +Returns the current `UserProfile` for the verified UID. `401` if token invalid. +The website dashboard (Phase 4) calls this after Firebase web sign-in and gates +analytics on `web_connected == true`. + +### POST /auth/connect (Phase 2 — explicit consent) + +Marks the verified UID as connected to the web dashboard. Called by the +Android app ONLY after the user taps "Connect Web Dashboard" and confirms. + +```http +POST /auth/connect HTTP/1.1 +Authorization: Bearer +Content-Type: application/json + +{} +``` + +Server behavior (private backend): + +1. Verify the Bearer token via Admin SDK — 401 if invalid/expired/revoked. +2. `UPDATE users SET web_connected = true, web_connected_at = COALESCE(web_connected_at, now()), last_seen_at = now() WHERE firebase_uid = $uid` (idempotent). +3. Return the updated `UserProfile`. +4. Uploads no usage data — this endpoint only flips the consent flag. + +### DELETE /auth/connect (Phase 2 — revoke consent) + +Clears `web_connected` for the verified UID (idempotent) and returns the +updated `UserProfile`. Same auth header, no body. The Android settings card +offers this as "Disconnect"; Phase 3+ sync must stop for the UID afterwards. + +### POST /events/batch (Phase 3 — usage ingestion, gated) + +Accepts `{ device, events[] }` (1–500 items) with per-item partial success: +`{ accepted, duplicates, rejected[], device_id }`. Each event carries an +optional `source_page` naming the in-app surface that produced it +(`cloud_usage_dashboard` | `developer_page`). Retries reuse the same +`event_id` and dedupe server-side. 403 `web_not_connected` for unlinked users. + +### POST /analytics/snapshot (Phase 3 revised — page rollups, gated) + +Accepts one page-level snapshot per call: +`{ device, source_page, snapshot_type, snapshot_id, payload }`, where +`source_page` is `cloud_usage_dashboard` or `developer_page` and +`snapshot_id` is a stable hourly bucket so retries return +`{ stored: false, duplicate: true }` instead of double-storing. Payloads are +capped at 100KB. 403 `web_not_connected` for unlinked users. + +Android collection (no UI changes — existing screens only): +- Cloud usage dashboard → `CloudUsageMeter` records (stable UUID ids) become + `cloud`-engine events; the dashboard rollup becomes a `dashboard_rollup` snapshot. +- Developer page → `TelemetryRepository` generations become `local`-engine + `chat_completed` events (output ⇒ success); the telemetry summary becomes a + `telemetry_summary` snapshot. +- Uploads run in `AnalyticsSyncWorker` (WorkManager, 6-hourly, network-required) + and only after `GET /me` confirms `web_connected`; watermarks in DataStore + skip already-sent windows. +- Freshness: every recorded cloud request and completed local generation fires + a listener that `AnalyticsSyncTrigger` debounces (~8s) into a one-shot upload, + so usage lands in Supabase within ~10s; the website silently re-fetches every + 10s while visible. Bursts coalesce; offline/guest runs no-op. + +--- + +## 3. UserProfile fields (Phase 1) + +| Field | Type | Notes | +|---|---|---| +| `id` | uuid PK | server-generated, never client-supplied | +| `firebase_uid` | text UNIQUE NOT NULL | canonical key, from verified token | +| `email` | text nullable | from verified token | +| `display_name` | text nullable | from verified token | +| `photo_url` | text nullable | from verified token | +| `created_at` | timestamptz NOT NULL | set once | +| `last_seen_at` | timestamptz NOT NULL | updated on every `/auth/verify` | +| `sync_enabled` | bool NOT NULL DEFAULT false | Phase 1 default false; Phase 2/3 flip only after explicit connect | +| `settings` | jsonb NOT NULL DEFAULT '{}' | per-user prefs, opaque to auth | +| `web_connected` | bool NOT NULL DEFAULT false | added/used in Phase 2 (present in template schema for forward compat) | +| `web_connected_at` | timestamptz nullable | set once on explicit connect (Phase 2) | + +Full DDL: [`schema-identity.sql`](schema-identity.sql). + +--- + +## 4. Security rules (must-follow) + +1. Never trust client-supplied user IDs — only `verifiedToken.uid`. +2. Trust only verified Firebase ID tokens (Admin SDK). Check expiry/revocation. +3. Use Firebase UID as source of truth for every row (`WHERE firebase_uid = $uid`). +4. No usage/analytics sync before explicit app-to-web connection (enforced Phase 2+). +5. Website shows only the current user's data — no admin/global reads on these endpoints. +6. Backend secrets stay private: service-account JSON and `.env` are never committed. + The public repo contains only `.env.example` files and a backend **template**. + +--- + +## 5. What lives where (repo split) + +| Location | Contents | +|---|---| +| Public repo (`androllm`) | This contract, `schema-identity.sql`, Android `core/network/identity/` client, website `lib/firebase-client.ts` + `lib/backend-client.ts` + `hooks/use-backend-user.ts`, `website/.env.example`, backend **template** under `tools/backend-template/` (no secrets) | +| Private backend repo | Copy of `tools/backend-template/` + real `.env` + service-account JSON (gitignored). The only place Admin SDK keys exist. | + +See `tools/backend-template/README.md` for private-repo setup. + +--- + +## 6. Phase boundaries (do not cross) + +- Phase 1: this document + 3 endpoints + profile. No events, no sync, no charts. +- Phase 2: adds the Android "Connect Web Dashboard" consent + `web_connected` gating. +- Phase 3: adds event ingestion (only for `web_connected = true` users). +- Phase 4: adds the website analytics dashboard (only for connected accounts). diff --git a/documentation/backend/schema-identity.sql b/documentation/backend/schema-identity.sql new file mode 100644 index 00000000..35651ab2 --- /dev/null +++ b/documentation/backend/schema-identity.sql @@ -0,0 +1,35 @@ +-- AndroLLM Identity Foundation — Phase 1 schema (PostgreSQL) +-- Private backend only. No secrets in this file — safe to keep as reference +-- in the public repo. Apply with: psql "$DATABASE_URL" -f schema-identity.sql +-- +-- Phase 1: identity only (users table). No events/sessions/devices yet (Phase 3). +-- web_connected columns are included for forward compatibility but MUST stay +-- false until Phase 2 explicit consent flips them. + +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; + +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + firebase_uid TEXT NOT NULL UNIQUE, + email TEXT, + display_name TEXT, + photo_url TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(), + sync_enabled BOOLEAN NOT NULL DEFAULT FALSE, + settings JSONB NOT NULL DEFAULT '{}'::jsonb, + -- Phase 2 consent flags (default false in Phase 1; enforced by API, not just DDL) + web_connected BOOLEAN NOT NULL DEFAULT FALSE, + web_connected_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_users_last_seen_at ON users (last_seen_at DESC); + +-- Safety: firebase_uid must never be empty. +ALTER TABLE users + ADD CONSTRAINT users_firebase_uid_nonempty CHECK (firebase_uid <> ''); + +COMMENT ON TABLE users IS 'Phase 1 identity: one row per Firebase UID (canonical key = firebase_uid).'; +COMMENT ON COLUMN users.firebase_uid IS 'Verified Firebase UID from Admin SDK verifyIdToken(). Never client-supplied.'; +COMMENT ON COLUMN users.sync_enabled IS 'Phase 1 always false. Phase 2/3 enable only after explicit Connect Web Dashboard consent.'; +COMMENT ON COLUMN users.web_connected IS 'Phase 2 consent flag. Phase 1 backend never sets it true.'; diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index dada7674..84839851 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { implementation(project(":core:models")) implementation(project(":core:database")) implementation(project(":core:datastore")) + implementation(project(":core:network")) implementation(project(":core:memory")) implementation(project(":core:cloud")) implementation(project(":core:attachments")) diff --git a/feature/settings/src/main/java/io/androllm/feature/settings/SettingsAccordion.kt b/feature/settings/src/main/java/io/androllm/feature/settings/SettingsAccordion.kt index 3296a759..c292496b 100644 --- a/feature/settings/src/main/java/io/androllm/feature/settings/SettingsAccordion.kt +++ b/feature/settings/src/main/java/io/androllm/feature/settings/SettingsAccordion.kt @@ -109,7 +109,8 @@ internal val SettingsGroup.title: String internal val SettingsGroup.keywords: List get() = when (this) { SettingsGroup.Account -> listOf( - "sign in", "google", "sync", "profile", "guest", "account", "firebase", "email" + "sign in", "google", "sync", "profile", "guest", "account", "firebase", "email", + "web dashboard", "connect", "link", "analytics", "website" ) SettingsGroup.Appearance -> listOf( "text size", "density", "blur", diff --git a/feature/settings/src/main/java/io/androllm/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/io/androllm/feature/settings/SettingsScreen.kt index 13cc1c42..7a634e3c 100644 --- a/feature/settings/src/main/java/io/androllm/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/io/androllm/feature/settings/SettingsScreen.kt @@ -249,6 +249,9 @@ fun SettingsScreen( user = user, onSignIn = { navController.navigate(io.androllm.core.navigation.Routes.AUTH) } ) + WebDashboardConnectorCard( + onSignIn = { navController.navigate(io.androllm.core.navigation.Routes.AUTH) } + ) } settingsAccordionItem( @@ -874,6 +877,153 @@ private fun FirebaseAuthCard( } } +/** + * Phase 2 — Web Dashboard connector. + * + * Explicit opt-in linking: the user taps "Connect Web Dashboard", confirms in + * a dialog, and only then does the app call POST /auth/connect for the + * verified Firebase UID. No usage data is uploaded by connecting — it only + * unlocks future analytics sync (Phase 3) and the website dashboard (Phase 4). + */ +@Composable +private fun WebDashboardConnectorCard( + onSignIn: () -> Unit, + viewModel: WebDashboardViewModel = hiltViewModel() +) { + val state by viewModel.state.collectAsStateWithLifecycle() + + CloudGlassCard(modifier = Modifier.fillMaxWidth()) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Web Dashboard", + style = MaterialTheme.typography.titleSmall.copy( + fontWeight = FontWeight.Bold, + color = MaterialTheme.ledger.deskPaper + ) + ) + Text( + text = when { + !state.backendConfigured -> "Web sync is not configured in this build" + !state.signedIn -> "Sign in to link this device to the web dashboard" + state.connected -> "This device is linked to your web dashboard" + else -> "Link this device to see your stats on the web" + }, + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.ledger.deskInk + ), + maxLines = 2, + overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.width(12.dp)) + CloudChip( + text = when { + !state.backendConfigured -> "Unavailable" + !state.signedIn -> "Sign-in required" + state.connected -> "Connected" + else -> "Not connected" + }, + accentColor = if (state.connected) MaterialTheme.ledger.lampDeep + else MaterialTheme.ledger.deskInk + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Connecting allows future uploads of usage totals " + + "(chat counts, token usage, latency). Nothing is uploaded " + + "until you connect — local and cloud AI work the same either way.", + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.ledger.deskInk + ) + ) + if (state.connected) { + val connectedAt = state.connectedAt + if (connectedAt != null) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Connected since ${connectedAt.take(10)}", + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.ledger.deskInk + ) + ) + } + } + val connectorError = state.error + if (connectorError != null) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = connectorError, + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.error + ) + ) + } + Spacer(modifier = Modifier.height(16.dp)) + when { + !state.backendConfigured -> Unit // Note above is enough. + !state.signedIn -> CloudCapsuleButton( + text = "Sign in to connect", + onClick = onSignIn, + icon = Icons.Filled.AccountCircle, + modifier = Modifier.fillMaxWidth() + ) + state.connected -> TextButton( + onClick = { viewModel.disconnect() }, + enabled = !state.busy, + modifier = Modifier.fillMaxWidth() + ) { + Text(if (state.busy) "Working…" else "Disconnect web dashboard") + } + else -> CloudCapsuleButton( + text = if (state.busy) "Working…" else "Connect Web Dashboard", + onClick = { viewModel.requestConnect() }, + enabled = !state.busy, + icon = Icons.Filled.Insights, + modifier = Modifier.fillMaxWidth() + ) + } + } + } + + if (state.showConfirm) { + AlertDialog( + onDismissRequest = { viewModel.dismissConfirm() }, + title = { + Text( + text = "Connect Web Dashboard?", + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold) + ) + }, + text = { + Text( + text = "This links your Firebase account to the AndroLLM web dashboard. " + + "In the future the app may upload usage totals (number of chats, " + + "token counts, model and latency stats) so the website can show " + + "your analytics. No chat content is ever uploaded, and you can " + + "disconnect at any time from this screen.", + style = MaterialTheme.typography.bodyMedium + ) + }, + confirmButton = { + TextButton(onClick = { viewModel.confirmConnect() }) { + Text("Connect") + } + }, + dismissButton = { + TextButton(onClick = { viewModel.dismissConfirm() }) { + Text("Not now") + } + } + ) + } +} + @Composable internal fun SettingRow( icon: ImageVector, diff --git a/feature/settings/src/main/java/io/androllm/feature/settings/WebDashboardViewModel.kt b/feature/settings/src/main/java/io/androllm/feature/settings/WebDashboardViewModel.kt new file mode 100644 index 00000000..ee1e5297 --- /dev/null +++ b/feature/settings/src/main/java/io/androllm/feature/settings/WebDashboardViewModel.kt @@ -0,0 +1,190 @@ +package io.androllm.feature.settings + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.firebase.auth.FirebaseAuth +import dagger.hilt.android.lifecycle.HiltViewModel +import io.androllm.core.datastore.PreferencesDataStore +import io.androllm.core.network.identity.IdentityApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +/** + * Phase 2 — Web Dashboard connector state. + * + * Explicit account linking only. This ViewModel never uploads usage data; + * [IdentityApi.connect] flips the backend `web_connected` flag for the + * verified Firebase UID and nothing else. Phase 3 sync must additionally + * check `UserProfile.canSyncUsage()` before sending any event. + * + * Source of truth is the backend profile (`GET /me`); the DataStore copy is + * a display cache so the card renders instantly and offline. + */ +data class WebDashboardState( + val signedIn: Boolean = false, + val backendConfigured: Boolean = false, + val connected: Boolean = false, + val connectedAt: String? = null, + /** True once this session confirmed the state with the backend. */ + val isFresh: Boolean = false, + val busy: Boolean = false, + val error: String? = null, + val showConfirm: Boolean = false, +) + +@HiltViewModel +class WebDashboardViewModel @Inject constructor( + private val identityApi: IdentityApi, + private val preferencesDataStore: PreferencesDataStore, +) : ViewModel() { + + private val auth: FirebaseAuth? = runCatching { FirebaseAuth.getInstance() }.getOrNull() + + private val _state = MutableStateFlow( + WebDashboardState(backendConfigured = identityApi.isConfigured) + ) + val state: StateFlow = _state.asStateFlow() + + private val authListener = FirebaseAuth.AuthStateListener { firebaseAuth -> + val signedIn = firebaseAuth.currentUser != null + _state.update { it.copy(signedIn = signedIn, error = null, isFresh = false) } + if (signedIn) refresh() else _state.update { it.copy(busy = false, showConfirm = false) } + } + + init { + // Instant/offline render from the local cache. + viewModelScope.launch { + combine( + preferencesDataStore.webConnected, + preferencesDataStore.webConnectedAt, + ) { connected, at -> connected to at } + .collect { (connected, at) -> + _state.update { + // Backend-fresh state wins over the cache within a session. + if (it.isFresh) it else it.copy(connected = connected, connectedAt = at) + } + } + } + _state.update { it.copy(signedIn = runCatching { auth?.currentUser != null }.getOrDefault(false)) } + auth?.addAuthStateListener(authListener) + if (_state.value.signedIn) refresh() + } + + override fun onCleared() { + super.onCleared() + auth?.removeAuthStateListener(authListener) + } + + /** Reconcile with the backend profile. Safe to call on every settings open. */ + fun refresh() { + if (!identityApi.isConfigured) return + if (runCatching { auth?.currentUser == null }.getOrDefault(true)) return + viewModelScope.launch { + _state.update { it.copy(busy = true, error = null) } + when (val result = identityApi.me()) { + is IdentityApi.IdentityResult.Success -> { + preferencesDataStore.setWebConnection( + result.value.webConnected, + result.value.webConnectedAt, + ) + _state.update { + it.copy( + connected = result.value.webConnected, + connectedAt = result.value.webConnectedAt, + isFresh = true, + busy = false, + ) + } + } + is IdentityApi.IdentityResult.Unauthorized -> + _state.update { it.copy(busy = false, error = "Session expired — please sign in again.") } + is IdentityApi.IdentityResult.Failure -> { + Timber.w("[WebDashboard] refresh failed: ${result.message}") + // Keep the cached state; surface a soft error only. + _state.update { it.copy(busy = false) } + } + } + } + } + + /** Open the explicit-approval dialog. No network call happens here. */ + fun requestConnect() { + val current = _state.value + if (!current.signedIn || !current.backendConfigured || current.connected || current.busy) return + _state.update { it.copy(showConfirm = true, error = null) } + } + + fun dismissConfirm() { + _state.update { it.copy(showConfirm = false) } + } + + /** User confirmed — perform the one-time linking call. Uploads no usage data. */ + fun confirmConnect() { + if (_state.value.busy) return + viewModelScope.launch { + _state.update { it.copy(busy = true, error = null, showConfirm = false) } + when (val result = identityApi.connect()) { + is IdentityApi.IdentityResult.Success -> { + preferencesDataStore.setWebConnection( + result.value.webConnected, + result.value.webConnectedAt, + ) + _state.update { + it.copy( + connected = result.value.webConnected, + connectedAt = result.value.webConnectedAt, + isFresh = true, + busy = false, + ) + } + } + is IdentityApi.IdentityResult.Unauthorized -> + _state.update { it.copy(busy = false, error = "Session expired — please sign in again.") } + is IdentityApi.IdentityResult.Failure -> { + Timber.w("[WebDashboard] connect failed: ${result.message}") + _state.update { it.copy(busy = false, error = "Couldn't connect. Check your connection and try again.") } + } + } + } + } + + /** Revoke linking. Idempotent; clears the local cache on success. */ + fun disconnect() { + if (_state.value.busy) return + viewModelScope.launch { + _state.update { it.copy(busy = true, error = null) } + when (val result = identityApi.disconnect()) { + is IdentityApi.IdentityResult.Success -> { + preferencesDataStore.setWebConnection( + result.value.webConnected, + result.value.webConnectedAt, + ) + _state.update { + it.copy( + connected = result.value.webConnected, + connectedAt = result.value.webConnectedAt, + isFresh = true, + busy = false, + ) + } + } + is IdentityApi.IdentityResult.Unauthorized -> + _state.update { it.copy(busy = false, error = "Session expired — please sign in again.") } + is IdentityApi.IdentityResult.Failure -> { + Timber.w("[WebDashboard] disconnect failed: ${result.message}") + _state.update { it.copy(busy = false, error = "Couldn't disconnect. Check your connection and try again.") } + } + } + } + } + + fun clearError() { + _state.update { it.copy(error = null) } + } +} diff --git a/website/.dockerignore b/website/.dockerignore index 0c40d65c..d31b5a3f 100644 --- a/website/.dockerignore +++ b/website/.dockerignore @@ -1,20 +1,15 @@ -# Dependencies node_modules - -# Next.js build output .next out -tsconfig.tsbuildinfo - -# Logs and local env -*.log +dist +coverage .env .env.* -!.env.example - -# VCS and editor cruft +*.local +npm-debug.log* +.vercel .git .gitignore -.github -.idea -.vscode +Dockerfile +.dockerignore +README.md diff --git a/website/.env.example b/website/.env.example new file mode 100644 index 00000000..98b9a0eb --- /dev/null +++ b/website/.env.example @@ -0,0 +1,13 @@ +# Copy to .env.local for local dev. Never commit .env.local (gitignored). +# Firebase web config is PUBLIC by design (same project as the Android app). +# Get values from: Firebase Console → Project Settings → Your Apps → Web app. +NEXT_PUBLIC_FIREBASE_API_KEY= +NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=androllm.firebaseapp.com +NEXT_PUBLIC_FIREBASE_PROJECT_ID=androllm +NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=androllm.firebasestorage.app +NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID= +NEXT_PUBLIC_FIREBASE_APP_ID= + +# Private backend base URL (no trailing slash). Empty = backend disabled. +# Examples: http://localhost:8080 (local) or https://api.androllm.app (prod). +NEXT_PUBLIC_BACKEND_URL=http://localhost:8080 diff --git a/website/.gitignore b/website/.gitignore index 4c5eeaf4..ba2afdb8 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -17,8 +17,13 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -# env +# env (never commit real values; only .env.example is tracked) +.env .env*.local +# Firebase Admin / service-account keys must never live in the public repo +service-account*.json +*serviceAccount*.json +*-key.json # vercel .vercel diff --git a/website/Dockerfile b/website/Dockerfile index 70c57450..db27be15 100644 --- a/website/Dockerfile +++ b/website/Dockerfile @@ -1,61 +1,34 @@ -# Multi-stage production Dockerfile for the AndroLLM website (Next.js App Router). -# -# Coolify auto-detects this Dockerfile and uses it instead of its Nixpacks -# buildpack. Everything the runtime needs is pinned here: -# - exact Node.js version -# - exact build command (npm run build -> next build with output: "standalone") -# - exact start command (node server.js from .next/standalone) -# - port (EXPOSE 3000) and bind address (HOSTNAME=0.0.0.0) -# Coolify reads EXPOSE to configure its reverse proxy target, so the proxied -# port can never drift from the port the server actually listens on. - -# ---- Stage 1: install dependencies ---- +# AndroLLM website — Coolify / self-host production image. +# Next.js standalone output: the runner needs only node, no dev tooling. +# IMPORTANT: NEXT_PUBLIC_* values are inlined at BUILD time — set them as +# Docker build args (Coolify: Service → Build → Build Arguments). FROM node:22-alpine AS deps WORKDIR /app -# Install production deps first so layer caching works and the final runtime -# image only contains what the app actually needs at runtime. COPY package.json package-lock.json ./ RUN npm ci -# ---- Stage 2: build ---- FROM node:22-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . +ARG NEXT_PUBLIC_FIREBASE_API_KEY="" +ARG NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="" +ARG NEXT_PUBLIC_FIREBASE_PROJECT_ID="" +ARG NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="" +ARG NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="" +ARG NEXT_PUBLIC_FIREBASE_APP_ID="" +ARG NEXT_PUBLIC_BACKEND_URL="" ENV NEXT_TELEMETRY_DISABLED=1 RUN npm run build -# ---- Stage 3: runtime ---- FROM node:22-alpine AS runner WORKDIR /app - -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 -# Bind to all interfaces so the container's reverse proxy can reach the app -# (never 127.0.0.1, which is unreachable from outside the container). -ENV HOSTNAME=0.0.0.0 -# Port used by the standalone server. Coolify overrides PORT at runtime; 3000 -# is the default that matches EXPOSE below. -ENV PORT=3000 - -EXPOSE 3000 - -# Run as an unprivileged user. -RUN addgroup --system --gid 1001 nodejs \ - && adduser --system --uid 1001 nextjs - -# Standalone output: self-contained server.js + traced node_modules. +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3000 \ + HOSTNAME="0.0.0.0" +COPY --from=builder /app/public ./public COPY --from=builder /app/.next/standalone ./ -# Static assets and public files are NOT traced into the standalone output; -# they must be copied explicitly or every asset 404s. COPY --from=builder /app/.next/static ./.next/static -COPY --from=builder /app/public ./public - -USER nextjs - -# Health check against the homepage so orchestrators/proxies can verify the -# server is actually serving (Coolify performs its own check on EXPOSE too). -HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ - CMD node -e "fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + '/').then(r => process.exit(r.status === 200 ? 0 : 1)).catch(() => process.exit(1))" - +EXPOSE 3000 CMD ["node", "server.js"] diff --git a/website/app/account/page.tsx b/website/app/account/page.tsx new file mode 100644 index 00000000..ab7ec66a --- /dev/null +++ b/website/app/account/page.tsx @@ -0,0 +1,168 @@ +"use client"; + +/** + * Phase 2 — account linking status (no charts, no analytics yet). + * + * Same Firebase project as the Android app. After sign-in, the backend + * profile (`GET /auth/session`) tells whether the Android app was explicitly + * connected via "Connect Web Dashboard": + * - `web_connected == true` → linked, analytics unlock in Phase 4. + * - otherwise → "Connect your Android app to enable analytics." + */ +import Link from "next/link"; +import { useState } from "react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + isBackendConfigured, +} from "@/lib/backend-client"; +import { + isFirebaseConfigured, + signOut, +} from "@/lib/firebase-client"; +import { useBackendUser } from "@/hooks/use-backend-user"; + +export default function AccountPage() { + const { firebaseUser, profile, loading, error, refresh } = useBackendUser(); + const [busy, setBusy] = useState(false); + const [actionError, setActionError] = useState(null); + + async function handleAuthAction(action: () => Promise) { + setBusy(true); + setActionError(null); + try { + await action(); + await refresh(); + } catch (e) { + setActionError(e instanceof Error ? e.message : "Something went wrong."); + } finally { + setBusy(false); + } + } + + if (!isFirebaseConfigured()) { + return ( +
+ + + Account + + Firebase web config is missing. Add the public + NEXT_PUBLIC_FIREBASE_* values to .env.local (see .env.example). + + + +
+ ); + } + + const linked = profile?.web_connected === true; + + return ( +
+ + +
+ Account + {firebaseUser ? ( + + {loading ? "Checking…" : linked ? "Linked" : "Not linked"} + + ) : ( + Signed out + )} +
+ + Sign in with the same account you use in the AndroLLM Android app. + +
+ + {!firebaseUser ? ( + <> +

+ Use the same Firebase account on both the app and this site — + your UID is the shared identity key. +

+
+ +
+ + ) : !isBackendConfigured() ? ( + <> +

+ Signed in as {firebaseUser.email ?? "your account"}. The private + backend is not configured (NEXT_PUBLIC_BACKEND_URL), so link + status is unavailable. +

+ + + ) : loading ? ( +

+ Checking link status… +

+ ) : linked ? ( + <> +

+ Your Android app is connected + {profile?.web_connected_at + ? ` (since ${profile.web_connected_at.slice(0, 10)})` + : ""} + . Your analytics will appear here once sync is available. +

+
+ + +
+ + ) : ( + <> +

+ Connect your Android app to enable analytics. In the app, go to + Settings → Account & Sync →{" "} + Connect Web Dashboard and confirm. +

+
+ + +
+ + )} + {(error ?? actionError) && ( +

{error ?? actionError}

+ )} +
+
+
+ ); +} diff --git a/website/app/dashboard/page.tsx b/website/app/dashboard/page.tsx new file mode 100644 index 00000000..371e4d52 --- /dev/null +++ b/website/app/dashboard/page.tsx @@ -0,0 +1,15 @@ +/** + * Phase 4 — personal analytics dashboard (metadata + client shell). + * Data shown is ONLY the signed-in Firebase user's own usage. + */ +import type { Metadata } from "next"; +import { DashboardScreen } from "./screen"; + +export const metadata: Metadata = { + title: "Dashboard", + description: "Your personal AndroLLM usage analytics — chats, tokens, models, and activity.", +}; + +export default function DashboardPage() { + return ; +} diff --git a/website/app/dashboard/screen.test.tsx b/website/app/dashboard/screen.test.tsx new file mode 100644 index 00000000..7c0da15a --- /dev/null +++ b/website/app/dashboard/screen.test.tsx @@ -0,0 +1,93 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { DashboardScreen } from "@/app/dashboard/screen"; + +vi.mock("next/link", async () => { + const React = await import("react"); + return { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + default: ({ children, href }: any) => + React.createElement("a", { href: typeof href === "string" ? href : "#" }, children), + }; +}); + +const userHook = vi.hoisted(() => ({ mock: vi.fn() })); +const dashHook = vi.hoisted(() => ({ mock: vi.fn() })); + +vi.mock("@/hooks/use-backend-user", () => ({ useBackendUser: userHook.mock })); +vi.mock("@/hooks/use-dashboard", () => ({ useDashboard: dashHook.mock })); + +const signedIn = { + firebaseUser: { email: "a@example.com" }, + profile: { web_connected: true, web_connected_at: "2026-09-20T00:00:00.000Z" }, + loading: false, + error: null, + refresh: async () => {}, +}; + +describe("DashboardScreen states", () => { + it("shows the connect prompt for unlinked accounts (no charts)", () => { + userHook.mock.mockReturnValue({ ...signedIn, profile: { web_connected: false } }); + dashHook.mock.mockReturnValue({ status: "not-connected" }); + render(); + expect(screen.getByTestId("dashboard-not-connected")).toBeDefined(); + expect(screen.getAllByText(/Connect your Android app/i).length).toBeGreaterThan(0); + expect(screen.queryByTestId("dashboard-charts")).toBeNull(); + expect(screen.queryByTestId("kpi-grid")).toBeNull(); + }); + + it("shows the empty onboarding state for linked accounts without usage", () => { + userHook.mock.mockReturnValue(signedIn); + dashHook.mock.mockReturnValue({ status: "empty", refresh: async () => {} }); + render(); + expect(screen.getByTestId("dashboard-empty")).toBeDefined(); + expect(screen.getByText(/No analytics yet/i)).toBeDefined(); + }); + + it("renders cards and activity for linked accounts with data", () => { + userHook.mock.mockReturnValue(signedIn); + dashHook.mock.mockReturnValue({ + status: "ready", + summary: { + totals: { + chats: 3, events: 3, total_tokens: 800, input_tokens: 250, output_tokens: 550, + local_tokens: 600, cloud_tokens: 200, sessions: 1, + avg_latency_ms: 1667, avg_ttft_ms: 400, failed: 1, + }, + }, + charts: { + per_day: [ + { day: "2026-09-21", chats: 3, events: 3, input_tokens: 250, output_tokens: 550, total_tokens: 800, local_tokens: 600, cloud_tokens: 200, local_chats: 2, cloud_chats: 1, successes: 2, sessions: 1, errors: 1, avg_latency_ms: 1667, avg_ttft_ms: 400 }, + ], + models: [ + { model_name: "qwen3-0.6b", provider_name: null, engine_type: "local", chats: 2, events: 2, total_tokens: 600, avg_latency_ms: 1000, failed: 0 }, + ], + heatmap: [], + providers: [], + by_device: [], + app_versions: [], + token_histogram: [], + session_durations: [], + }, + activity: { + events: [ + { event_id: "e1", event_type: "chat_completed", model_name: "qwen3-0.6b", provider_name: null, engine_type: "local", total_tokens: 300, latency_ms: 1000, success: true, error_code: null, device_name: "Pixel", created_at: "2026-09-21T13:00:00.000Z" }, + ], + sessions: [], + devices: [ + { id: "d1", device_identifier: "dev1", device_name: "Pixel", platform: "android", app_version: "1.1.6", last_seen_at: "2026-09-21T13:00:00.000Z" }, + ], + }, + refresh: async () => {}, + }); + render(); + expect(screen.getByTestId("dashboard-ready")).toBeDefined(); + expect(screen.getByTestId("kpi-grid")).toBeDefined(); + expect(screen.getByTestId("dashboard-charts")).toBeDefined(); + expect(screen.getAllByText("qwen3-0.6b").length).toBeGreaterThan(0); + fireEvent.click(screen.getByRole("tab", { name: "Activity" })); + expect(screen.getByTestId("dashboard-activity")).toBeDefined(); + fireEvent.click(screen.getByRole("tab", { name: "Models" })); + expect(screen.getByText("Model leaderboard")).toBeDefined(); + }); +}); diff --git a/website/app/dashboard/screen.tsx b/website/app/dashboard/screen.tsx new file mode 100644 index 00000000..3cc22e5f --- /dev/null +++ b/website/app/dashboard/screen.tsx @@ -0,0 +1,458 @@ +"use client"; + +/** + * Phase 4 expanded — full-page analytics experience. + * + * One scrollable page, five tabs (Overview / Models / Patterns / Fleet / + * Activity), a filter bar (time range, engine, model), and a data-notes + * footer. Everything shown is the signed-in user's own usage; connection, + * loading, error, and empty states match the original screen's testids. + */ +import Link from "next/link"; +import { useMemo, useState } from "react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { ActivityView } from "@/components/dashboard/activity"; +import { DashboardChartsView } from "@/components/dashboard/charts"; +import { KpiGrid } from "@/components/dashboard/kpi"; +import { + AppVersionChart, + DayOfWeekChart, + Heatmap, + HourlyChart, + LocalCloudChats, + ModelErrorChart, + ModelSpeedChart, + ProviderChart, + SessionDurationChart, + SuccessRateChart, + TokenHistogram, + TtftChart, + WeeklyChart, +} from "@/components/dashboard/patterns"; +import { + DayOfWeekNote, + FastestVsStable, + LocalVsCloud, + MonthHalves, + PeakVsOffHours, + WeekVsWeek, + WeekdayVsWeekend, +} from "@/components/dashboard/compare"; +import { + DeviceLeaderboard, + ErrorLeaderboard, + ModelLeaderboard, + SessionLeaderboard, +} from "@/components/dashboard/tables"; +import { useBackendUser } from "@/hooks/use-backend-user"; +import { useDashboard } from "@/hooks/use-dashboard"; +import { formatCompact, relativeTime, sliceRange, type RangeKey } from "@/lib/dashboard-format"; +import type { DashboardActivity, DashboardCharts, DashboardSummary } from "@/lib/dashboard-client"; + +type Tab = "overview" | "models" | "patterns" | "fleet" | "activity"; +type EngineFilter = "all" | "local" | "cloud"; + +const TABS: { id: Tab; label: string }[] = [ + { id: "overview", label: "Overview" }, + { id: "models", label: "Models" }, + { id: "patterns", label: "Patterns" }, + { id: "fleet", label: "Devices & Sessions" }, + { id: "activity", label: "Activity" }, +]; + +function Shell({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function SkeletonGrid() { + return ( +
+ {Array.from({ length: 7 }).map((_, i) => ( +
+ ))} +
+
+ ); +} + +export function DashboardScreen() { + const { firebaseUser, profile } = useBackendUser(); + const { status, summary, charts, activity, error, refresh } = useDashboard(); + + if (status === "loading") { + return ( + + + + ); + } + + if (status === "signed-out") { + return ( + + + + Sign in to view your dashboard + + Use the same Firebase account as your Android app. + + + + + + + + ); + } + + if (status === "not-connected") { + return ( + + + +
+ Connect your Android app + Not linked +
+ + Connect your Android app to start syncing analytics. + +
+ +
    +
  1. Open the AndroLLM app on your Android device.
  2. +
  3. Go to Settings → Account & Sync.
  4. +
  5. Tap Connect Web Dashboard and confirm.
  6. +
  7. Then come back here and refresh.
  8. +
+
+ + +
+
+
+
+ ); + } + + if (status === "error") { + return ( + + + + Couldn't load your dashboard + {error ?? "Something went wrong."} + + + + + + + ); + } + + if (status === "empty") { + return ( + + + +
+ No analytics yet + Linked +
+ + Your app is connected{firebaseUser?.email ? ` as ${firebaseUser.email}` : ""}. + Once you chat in the app, totals, charts, and activity will appear here. + +
+ +
    +
  • Chat with a local or cloud model in the app.
  • +
  • Keep the device online so queued events can sync.
  • +
  • Come back here — your stats update on refresh.
  • +
+
+ +
+
+
+
+ ); + } + + if (!summary || !charts || !activity) { + return ( + + + + ); + } + + return ( + void refresh()} + /> + ); +} + +function ReadyView({ + summary, + charts, + activity, + email, + connectedAt, + onRefresh, +}: { + summary: DashboardSummary; + charts: DashboardCharts; + activity: DashboardActivity; + email: string | null; + connectedAt: string | null; + onRefresh: () => void; +}) { + const [tab, setTab] = useState("overview"); + const [range, setRange] = useState(30); + const [engine, setEngine] = useState("all"); + const [model, setModel] = useState("all"); + + const ranged = useMemo(() => sliceRange(charts.per_day, range), [charts, range]); + const filteredModels = useMemo( + () => + charts.models.filter( + (m) => (engine === "all" || m.engine_type === engine) && (model === "all" || m.model_name === model), + ), + [charts, engine, model], + ); + const filteredActivity = useMemo( + () => ({ + ...activity, + events: activity.events.filter( + (e) => (engine === "all" || e.engine_type === engine) && (model === "all" || e.model_name === model), + ), + }), + [activity, engine, model], + ); + const platformSplit = useMemo(() => { + const map = new Map(); + for (const d of charts.by_device) { + map.set(d.platform, (map.get(d.platform) ?? 0) + d.chats); + } + return [...map.entries()]; + }, [charts]); + + return ( + + {/* Hero */} +
+
+
+

Usage intelligence

+ Linked +
+

+ {email ?? "Your account"} + {connectedAt ? ` · linked since ${connectedAt.slice(0, 10)}` : ""} + {summary.totals.last_synced_at ? ` · synced ${relativeTime(summary.totals.last_synced_at)}` : ""} +

+
+
+ + +
+
+ + {/* Filter bar */} +
+
+ {([7, 30, 90] as RangeKey[]).map((r) => ( + + ))} +
+ +
+ {(["all", "local", "cloud"] as EngineFilter[]).map((e) => ( + + ))} +
+ + + + {formatCompact(summary.totals.chats)} chats · {formatCompact(summary.totals.total_tokens)} tokens all time + +
+ + {/* Tabs */} +
+ {TABS.map((t) => ( + + ))} +
+ + {tab === "overview" && ( +
+ + +
+ + + +
+
+ )} + + {tab === "models" && ( +
+
+ + +
+
+ + +
+ +
+ )} + + {tab === "patterns" && ( +
+ + +
+ + +
+
+ + + +
+
+ + +
+
+ + +
+ +
+ )} + + {tab === "fleet" && ( +
+
+ + +
+
+ + + + Platforms + Chat share by platform (90d) + + + {platformSplit.length === 0 ? ( +

No platform data.

+ ) : ( + platformSplit.map(([platform, chats]) => ( +
+ {platform} + {formatCompact(chats)} chats +
+ )) + )} +
+

+ Sync sources:{" "} + {summary.totals.by_source.length > 0 + ? summary.totals.by_source.map((s) => `${s.source_page ?? "unknown"} (${formatCompact(s.events)})`).join(" · ") + : "—"} +

+
+
+
+
+
+ )} + + {tab === "activity" && ( +
+ + +
+ )} + + {/* Footer notes */} +
+ Data & sync notes +
    +
  • Everything here is your own synced usage — never another account's.
  • +
  • Headline cards are all-time; charts default to the selected range (windowed breakdowns cover 90d).
  • +
  • New generations sync within ~10s; this page refreshes itself every 10s while visible.
  • +
  • Prompt text is never uploaded — only counts, timings, model names, and error codes.
  • +
+
+
+ ); +} diff --git a/website/app/login/page.tsx b/website/app/login/page.tsx new file mode 100644 index 00000000..3313e20b --- /dev/null +++ b/website/app/login/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from "next"; +import { LoginScreen } from "./screen"; + +export const metadata: Metadata = { + title: "Sign in", + description: "Sign in to AndroLLM with Google or GitHub — the same account as your Android app.", +}; + +export default function LoginPage() { + return ; +} diff --git a/website/app/login/screen.tsx b/website/app/login/screen.tsx new file mode 100644 index 00000000..eb31047a --- /dev/null +++ b/website/app/login/screen.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { AuthComponent } from "@/components/ui/sign-up"; +import { Logo } from "@/components/logo"; +import { subscribeToAuthChanges } from "@/lib/firebase-client"; + +export function LoginScreen() { + const router = useRouter(); + + useEffect(() => { + const unsubscribe = subscribeToAuthChanges((user) => { + if (user) router.replace("/dashboard"); + }); + return unsubscribe; + }, [router]); + + return ( + } + brandName="AndroLLM" + redirectTo="/dashboard" + onAuthenticated={() => router.push("/dashboard")} + /> + ); +} diff --git a/website/components/dashboard/activity.tsx b/website/components/dashboard/activity.tsx new file mode 100644 index 00000000..53ba4138 --- /dev/null +++ b/website/components/dashboard/activity.tsx @@ -0,0 +1,113 @@ +"use client"; + +/** + * Phase 4 — recent activity, sessions, and devices (signed-in user only). + */ +import { CheckCircle2, Cpu, Smartphone, XCircle } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { formatCompact, formatMs, timeLabel } from "@/lib/dashboard-format"; +import type { DashboardActivity } from "@/lib/dashboard-client"; + +function Section({ title, desc, children }: { title: string; desc: string; children: React.ReactNode }) { + return ( + + + {title} + {desc} + + {children} + + ); +} + +export function ActivityView({ activity }: { activity: DashboardActivity }) { + return ( +
+
+ {activity.events.length === 0 ? ( +

No activity yet.

+ ) : ( + activity.events.map((e) => ( +
+ {e.success ? ( + + ) : ( + + )} +
+

+ {e.model_name ?? e.event_type} + {e.error_code ? {e.error_code} : null} +

+

+ {formatCompact(e.total_tokens)} tokens + {e.latency_ms !== null ? ` · ${formatMs(e.latency_ms)}` : ""} + {e.device_name ? ` · ${e.device_name}` : ""} +

+
+ {timeLabel(e.created_at)} +
+ )) + )} +
+ +
+
+ {activity.sessions.length === 0 ? ( +

No sessions yet.

+ ) : ( + activity.sessions.slice(0, 6).map((s) => ( +
+ +
+

+ {s.engine_type ?? "session"} · {s.events} event{s.events === 1 ? "" : "s"} +

+

+ {timeLabel(s.started_at)} + {s.device_name ? ` · ${s.device_name}` : ""} +

+
+ + {s.ended_at ? "Ended" : "Active"} + +
+ )) + )} +
+ +
+ {activity.devices.length === 0 ? ( +

No devices yet.

+ ) : ( + activity.devices.map((d) => ( +
+ +
+

+ {d.device_name ?? d.device_identifier} +

+

+ {d.platform} + {d.app_version ? ` · v${d.app_version}` : ""} · seen {timeLabel(d.last_seen_at)} +

+
+ Linked +
+ )) + )} +
+
+
+ ); +} diff --git a/website/components/dashboard/charts.tsx b/website/components/dashboard/charts.tsx new file mode 100644 index 00000000..bc50beba --- /dev/null +++ b/website/components/dashboard/charts.tsx @@ -0,0 +1,159 @@ +"use client"; + +/** + * Phase 4 — personal usage charts (Recharts, dark theme). + * All series come from the UID-scoped backend endpoints; nothing global. + */ +import { + Area, + AreaChart, + Bar, + BarChart, + CartesianGrid, + Cell, + Legend, + Line, + LineChart, + Pie, + PieChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { dayLabel, engineSplit, formatCompact } from "@/lib/dashboard-format"; +import type { DayRow, ModelRow } from "@/lib/dashboard-client"; + +const GRID = "rgba(255,255,255,0.08)"; +const TICK = { fill: "#a1a1aa", fontSize: 12 }; +const TOOLTIP = { + backgroundColor: "#18181b", + border: "1px solid rgba(255,255,255,0.12)", + borderRadius: 12, + color: "#f4f4f5", + fontSize: 12, +}; +const LOCAL = "#34d399"; +const CLOUD = "#60a5fa"; +const ACCENT = "#f59e0b"; +const ROSE = "#fb7185"; + +function Panel({ title, desc, children }: { title: string; desc: string; children: React.ReactNode }) { + return ( + + + {title} + {desc} + + {children} + + ); +} + +export function DashboardChartsView({ charts }: { charts: { per_day: DayRow[]; models: ModelRow[] } }) { + const perDay = charts.per_day.map((d) => ({ + ...d, + label: dayLabel(d.day), + failureRate: d.events > 0 ? d.errors / d.events : 0, + })); + const split = engineSplit( + charts.per_day.reduce((n, d) => n + d.local_tokens, 0), + charts.per_day.reduce((n, d) => n + d.cloud_tokens, 0), + ); + const models = charts.models.slice(0, 8).map((m) => ({ + name: m.model_name.length > 18 ? `${m.model_name.slice(0, 17)}…` : m.model_name, + tokens: m.total_tokens, + chats: m.chats, + })); + + return ( +
+ + + + + + + formatCompact(Number(v))} /> + + + + + + + + + + + + + + + + + + + + + + + {split.length > 0 ? ( + + + + {split.map((s) => ( + + ))} + + formatCompact(Number(v))} /> + + + + ) : ( +

No token volume in this window.

+ )} +
+ + + {models.length > 0 ? ( + + + + + + formatCompact(Number(v))} /> + + + + ) : ( +

No model usage in this window.

+ )} +
+ + + + + + + `${Math.round(Number(v) / 100) / 10}k`} /> + `${Number(v).toLocaleString()} ms`} /> + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/website/components/dashboard/compare.tsx b/website/components/dashboard/compare.tsx new file mode 100644 index 00000000..61de72cc --- /dev/null +++ b/website/components/dashboard/compare.tsx @@ -0,0 +1,173 @@ +"use client"; + +/** + * Phase 4 expanded — head-to-head comparison cards. + * Every comparison is computed from the user's own series; deltas are honest + * (null-safe) and labeled with their windows. + */ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + dowName, + formatCompact, + formatGrowth, + growthRate, + hourLabel, + peakSplit, + sumDays, + weekdaySplit, +} from "@/lib/dashboard-format"; +import type { DayRow, HeatCell, ModelRow } from "@/lib/dashboard-client"; +import { Line, LineChart, ResponsiveContainer } from "recharts"; + +function Row({ label, left, right, delta }: { label: string; left: string; right: string; delta?: string }) { + return ( +
+ {label} + + {left} + vs + {right} + {delta ? {delta} : null} + +
+ ); +} + +function Panel({ title, desc, children }: { title: string; desc: string; children: React.ReactNode }) { + return ( + + + {title} + {desc} + + {children} + + ); +} + +export function WeekVsWeek({ perDay }: { perDay: DayRow[] }) { + const cur = perDay.slice(-7); + const prev = perDay.slice(-14, -7); + const rows: [string, (d: DayRow) => number][] = [ + ["Chats", (d) => d.chats], + ["Tokens", (d) => d.total_tokens], + ["Sessions", (d) => d.sessions], + ["Failures", (d) => d.errors], + ]; + return ( + + {rows.map(([label, pick]) => { + const c = sumDays(cur, pick); + const p = sumDays(prev, pick); + const g = formatGrowth(growthRate(c, p)); + return ; + })} +
+ + ({ v: d.chats }))}> + + + +
+
+ ); +} + +export function MonthHalves({ perDay }: { perDay: DayRow[] }) { + const cur = perDay.slice(-30); + const prev = perDay.slice(-60, -30); + const rows: [string, (d: DayRow) => number][] = [ + ["Chats", (d) => d.chats], + ["Tokens", (d) => d.total_tokens], + ["Sessions", (d) => d.sessions], + ]; + return ( + + {rows.map(([label, pick]) => { + const c = sumDays(cur, pick); + const p = sumDays(prev, pick); + const g = formatGrowth(growthRate(c, p)); + return ; + })} + + ); +} + +export function LocalVsCloud({ perDay }: { perDay: DayRow[] }) { + const lc = sumDays(perDay, (d) => d.local_chats); + const cc = sumDays(perDay, (d) => d.cloud_chats); + const lt = sumDays(perDay, (d) => d.local_tokens); + const ct = sumDays(perDay, (d) => d.cloud_tokens); + return ( + + + + 0 ? `${Math.round((lt / (lt + ct)) * 100)}%` : "—"} + right={lt + ct > 0 ? `${Math.round((ct / (lt + ct)) * 100)}%` : "—"} + /> + + ); +} + +export function WeekdayVsWeekend({ heatmap }: { heatmap: HeatCell[] }) { + const { weekday, weekend } = weekdaySplit(heatmap); + return ( + + + + + ); +} + +export function PeakVsOffHours({ heatmap }: { heatmap: HeatCell[] }) { + const { peak, off } = peakSplit(heatmap); + return ( + + +

+ Peak: {peak.hours.length > 0 ? peak.hours.map(hourLabel).join(", ") : "—"} +

+
+ ); +} + +export function FastestVsStable({ models }: { models: ModelRow[] }) { + const timed = models.filter((m) => m.avg_latency_ms !== null && m.chats > 0); + const fastest = [...timed].sort((a, b) => (a.avg_latency_ms ?? 0) - (b.avg_latency_ms ?? 0))[0]; + const stable = [...models.filter((m) => m.chats > 0)].sort((a, b) => { + const fa = mFailed(a); + const fb = mFailed(b); + return fa - fb; + })[0]; + function mFailed(m: ModelRow) { + return m.chats > 0 ? m.failed / m.chats : 1; + } + return ( + + + + + ); +} + +export function DayOfWeekNote({ heatmap }: { heatmap: HeatCell[] }) { + const byDow = new Map(); + for (const c of heatmap) byDow.set(c.dow, (byDow.get(c.dow) ?? 0) + c.chats); + const ranked = [...byDow.entries()].sort((a, b) => b[1] - a[1]); + return ( + +

+ {ranked.length > 0 ? dowName(ranked[0][0]) : "—"} +

+

+ {ranked.length > 0 ? `${formatCompact(ranked[0][1])} chats in window` : "No activity yet"} +

+
+ ); +} diff --git a/website/components/dashboard/kpi.test.tsx b/website/components/dashboard/kpi.test.tsx new file mode 100644 index 00000000..70674ef2 --- /dev/null +++ b/website/components/dashboard/kpi.test.tsx @@ -0,0 +1,49 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { KpiGrid } from "./kpi"; +import type { SummaryTotals } from "@/lib/dashboard-client"; + +const totals: SummaryTotals = { + chats: 12, + events: 15, + total_tokens: 2500, + input_tokens: 1000, + output_tokens: 1500, + local_tokens: 2000, + cloud_tokens: 500, + sessions: 4, + avg_latency_ms: 2500, + avg_ttft_ms: 800, + failed: 1, + devices: 1, + models_count: 2, + longest_session_seconds: 900, + peak_day: { day: "2026-09-21", chats: 5 }, + active_days: 3, + last_event_at: "2026-09-21T13:00:00.000Z", + last_synced_at: "2026-09-21T13:01:00.000Z", + by_source: [{ source_page: "cloud_usage_dashboard", events: 15 }], +}; + +const perDay = [ + { day: "2026-09-20", chats: 5, events: 6, input_tokens: 400, output_tokens: 600, total_tokens: 1000, local_tokens: 800, cloud_tokens: 200, local_chats: 4, cloud_chats: 1, successes: 6, sessions: 2, errors: 0, avg_latency_ms: 2000, avg_ttft_ms: 700 }, + { day: "2026-09-21", chats: 7, events: 9, input_tokens: 600, output_tokens: 900, total_tokens: 1500, local_tokens: 1200, cloud_tokens: 300, local_chats: 5, cloud_chats: 2, successes: 8, sessions: 2, errors: 1, avg_latency_ms: 3000, avg_ttft_ms: 900 }, +]; + +const models = [ + { model_name: "qwen3-0.6b", provider_name: null, engine_type: "local", chats: 9, events: 11, total_tokens: 2000, avg_latency_ms: 2500, failed: 0 }, +]; + +describe("KpiGrid", () => { + it("renders the expanded headline cards", () => { + render(); + expect(screen.getByTestId("kpi-grid")).toBeDefined(); + expect(screen.getByText("Total chats")).toBeDefined(); + expect(screen.getByText("12")).toBeDefined(); + expect(screen.getByText("2.5K")).toBeDefined(); + expect(screen.getByText("Success rate")).toBeDefined(); + expect(screen.getByText("Day streak")).toBeDefined(); + expect(screen.getByText("Weekly growth")).toBeDefined(); + expect(screen.getByText("Monthly growth")).toBeDefined(); + }); +}); diff --git a/website/components/dashboard/kpi.tsx b/website/components/dashboard/kpi.tsx new file mode 100644 index 00000000..daf8aade --- /dev/null +++ b/website/components/dashboard/kpi.tsx @@ -0,0 +1,133 @@ +"use client"; + +/** + * Phase 4 expanded — headline KPI grid (18 cards). + * Account-wide totals come from the summary; trend cards derive from the + * 90-day series client-side. Nothing here is global — all personal. + */ +import { + Activity, + AlertTriangle, + ArrowDownRight, + ArrowUpRight, + CalendarDays, + CheckCircle2, + Cloud, + Cpu, + Crown, + Flame, + Gauge, + Layers, + MessagesSquare, + RefreshCw, + Smartphone, + Timer, + Trophy, +} from "lucide-react"; +import { Card, CardContent } from "@/components/ui/card"; +import { + currentStreak, + dayLabel, + failureRate, + formatCompact, + formatDuration, + formatGrowth, + formatMs, + formatPercent, + growthRate, + relativeTime, + sliceRange, + sumDays, +} from "@/lib/dashboard-format"; +import type { DayRow, ModelRow, SummaryTotals } from "@/lib/dashboard-client"; + +function Stat({ + icon: Icon, + label, + value, + sub, + trend, +}: { + icon: typeof Cpu; + label: string; + value: string; + sub?: string; + trend?: { text: string; up: boolean | null }; +}) { + const showTrend = trend !== undefined && trend.up !== null; + const TrendIcon = trend?.up ? ArrowUpRight : ArrowDownRight; + const trendColor = trend?.up ? "text-emerald-400" : "text-rose-400"; + const long = value.length > 12; + return ( + + + + + + {label} + + + + + {value} + + {showTrend ? ( + + + {trend.text} + + ) : null} + + {sub ? {sub} : null} + + + ); +} + +export function KpiGrid({ + totals, + perDay, + models, +}: { + totals: SummaryTotals; + perDay: DayRow[]; + models: ModelRow[]; +}) { + const rate = failureRate(totals.failed, totals.events); + const success = rate === null ? null : 1 - rate; + const last7 = sliceRange(perDay, 7); + const prev7 = perDay.slice(Math.max(0, perDay.length - 14), Math.max(0, perDay.length - 7)); + const last30 = sliceRange(perDay, 30); + const prev30 = perDay.slice(Math.max(0, perDay.length - 60), Math.max(0, perDay.length - 30)); + const chatsW = formatGrowth(growthRate(sumDays(last7, (d) => d.chats), sumDays(prev7, (d) => d.chats))); + const tokensM = formatGrowth(growthRate(sumDays(last30, (d) => d.total_tokens), sumDays(prev30, (d) => d.total_tokens))); + const top = models[0]; + const streak = currentStreak(perDay); + + return ( +
+ + + + + + + + + + + + + 14 ? `${top.model_name.slice(0, 13)}…` : top.model_name) : "—"} sub={top ? `${formatCompact(top.total_tokens)} tokens` : undefined} /> + + + + + +
+ ); +} diff --git a/website/components/dashboard/patterns.tsx b/website/components/dashboard/patterns.tsx new file mode 100644 index 00000000..9517f829 --- /dev/null +++ b/website/components/dashboard/patterns.tsx @@ -0,0 +1,359 @@ +"use client"; + +/** + * Phase 4 expanded — usage-pattern charts: weekly trend, activity heatmap, + * hour/day distributions, token histogram, session durations, providers, + * app versions, success-rate and first-token trends. + */ +import { + Bar, + BarChart, + CartesianGrid, + Cell, + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + DURATION_BUCKETS, + TOKEN_BUCKETS, + dayLabel, + dowName, + formatCompact, + groupByWeek, + hourLabel, + orderBuckets, +} from "@/lib/dashboard-format"; +import type { + AppVersionRow, + BucketCount, + DayRow, + HeatCell, + ModelRow, + ProviderRow, +} from "@/lib/dashboard-client"; + +const GRID = "rgba(255,255,255,0.08)"; +const TICK = { fill: "#a1a1aa", fontSize: 12 }; +const TOOLTIP = { + backgroundColor: "#18181b", + border: "1px solid rgba(255,255,255,0.12)", + borderRadius: 12, + color: "#f4f4f5", + fontSize: 12, +}; +const LOCAL = "#34d399"; +const CLOUD = "#60a5fa"; +const ACCENT = "#f59e0b"; +const VIOLET = "#a78bfa"; +const ROSE = "#fb7185"; + +function Panel({ title, desc, children }: { title: string; desc: string; children: React.ReactNode }) { + return ( + + + {title} + {desc} + + {children} + + ); +} + +function Empty({ what }: { what: string }) { + return

No {what} in this window.

; +} + +export function WeeklyChart({ perDay }: { perDay: DayRow[] }) { + const weeks = groupByWeek(perDay).slice(-12); + return ( + + {weeks.length > 0 ? ( + + + + + + + + + + + + + ) : ( + + )} + + ); +} + +export function Heatmap({ cells }: { cells: HeatCell[] }) { + const max = cells.reduce((n, c) => Math.max(n, c.chats), 0); + const grid: number[][] = Array.from({ length: 7 }, () => Array(24).fill(0)); + for (const c of cells) { + if (c.dow >= 0 && c.dow < 7 && c.hour >= 0 && c.hour < 24) grid[c.dow][c.hour] += c.chats; + } + if (max === 0) { + return ( + + + + ); + } + return ( + +
+
+ + {Array.from({ length: 24 }, (_, h) => ( + + {h % 3 === 0 ? hourLabel(h) : ""} + + ))} + {grid.map((row, dow) => ( + + ))} +
+
+
+ ); +} + +function RowCells({ dow, row, max }: { dow: number; row: number[]; max: number }) { + return ( + <> + {dowName(dow)} + {row.map((v, h) => ( + + ))} + + ); +} + +export function HourlyChart({ cells }: { cells: HeatCell[] }) { + const hours = Array.from({ length: 24 }, (_, hour) => ({ + label: hourLabel(hour), + chats: cells.filter((c) => c.hour === hour).reduce((n, c) => n + c.chats, 0), + })); + return ( + + + + + + + + + + + + ); +} + +export function DayOfWeekChart({ cells }: { cells: HeatCell[] }) { + const days = Array.from({ length: 7 }, (_, dow) => ({ + label: dowName(dow), + chats: cells.filter((c) => c.dow === dow).reduce((n, c) => n + c.chats, 0), + })); + return ( + + + + + + + + + + + + ); +} + +export function TokenHistogram({ buckets }: { buckets: BucketCount[] }) { + const data = orderBuckets(buckets, TOKEN_BUCKETS); + return ( + + + + + + + + + + + + ); +} + +export function SessionDurationChart({ buckets }: { buckets: BucketCount[] }) { + const data = orderBuckets(buckets, DURATION_BUCKETS); + return ( + + + + + + + + + + + + ); +} + +export function ProviderChart({ providers }: { providers: ProviderRow[] }) { + return ( + + {providers.length > 0 ? ( + + ({ name: p.provider_name, tokens: p.total_tokens, chats: p.chats }))} layout="vertical" margin={{ left: 8, right: 16 }}> + + + + formatCompact(Number(v))} /> + + + + ) : ( + + )} + + ); +} + +export function AppVersionChart({ versions }: { versions: AppVersionRow[] }) { + return ( + + {versions.length > 0 ? ( + + ({ name: `v${v.app_version}`, chats: v.chats, tokens: v.total_tokens }))} margin={{ left: -16, right: 8 }}> + + + + + + + + ) : ( + + )} + + ); +} + +export function SuccessRateChart({ perDay }: { perDay: DayRow[] }) { + const data = perDay.map((d) => ({ + label: dayLabel(d.day), + rate: d.events > 0 ? (100 * d.successes) / d.events : null, + })); + return ( + + + + + + `${v}%`} /> + (v === null ? "—" : `${Number(v).toFixed(1)}%`)} /> + + + + + ); +} + +export function TtftChart({ perDay }: { perDay: DayRow[] }) { + const data = perDay.map((d) => ({ label: dayLabel(d.day), ttft: d.avg_ttft_ms })); + return ( + + + + + + `${Math.round(Number(v) / 100) / 10}k`} /> + (v === null ? "—" : `${Number(v).toLocaleString()} ms`)} /> + + + + + ); +} + +export function LocalCloudChats({ perDay }: { perDay: DayRow[] }) { const data = perDay.map((d) => ({ label: dayLabel(d.day), Local: d.local_chats, Cloud: d.cloud_chats })); + return ( + + + + + + + + + + + + + + ); + } + +export function ModelSpeedChart({ models }: { models: ModelRow[] }) { + const data = models + .filter((m) => m.avg_latency_ms !== null) + .slice(0, 10) + .map((m) => ({ name: m.model_name.length > 16 ? `${m.model_name.slice(0, 15)}…` : m.model_name, ms: m.avg_latency_ms ?? 0 })); + return ( + + {data.length > 0 ? ( + + + + `${Math.round(Number(v) / 100) / 10}k`} /> + + `${Number(v).toLocaleString()} ms`} /> + + + + ) : ( + + )} + + ); +} + +export function ModelErrorChart({ models }: { models: ModelRow[] }) { + const data = models + .filter((m) => m.failed > 0) + .slice(0, 10) + .map((m) => ({ name: m.model_name.length > 16 ? `${m.model_name.slice(0, 15)}…` : m.model_name, failed: m.failed })); + return ( + + {data.length > 0 ? ( + + + + + + + + + + ) : ( + + )} + + ); +} diff --git a/website/components/dashboard/tables.tsx b/website/components/dashboard/tables.tsx new file mode 100644 index 00000000..b767dec3 --- /dev/null +++ b/website/components/dashboard/tables.tsx @@ -0,0 +1,156 @@ +"use client"; + +/** + * Phase 4 expanded — ranked leaderboards (models, devices, sessions, errors). + * Personal rows only; empty states per table. + */ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { formatCompact, formatDuration, formatMs } from "@/lib/dashboard-format"; +import type { + ActivityEvent, + ActivitySession, + DeviceUsageRow, + ModelRow, +} from "@/lib/dashboard-client"; + +function Table({ + head, + rows, + empty, +}: { + head: string[]; + rows: React.ReactNode[][]; + empty: string; +}) { + if (rows.length === 0) return

{empty}

; + return ( +
+ + + + {head.map((h, i) => ( + + ))} + + + + {rows.map((cells, r) => ( + + {cells.map((c, i) => ( + + ))} + + ))} + +
0 ? "text-right" : ""}`}> + {h} +
0 ? "text-right tabular-nums" : ""}`}> + {c} +
+
+ ); +} + +function Panel({ title, desc, children }: { title: string; desc: string; children: React.ReactNode }) { + return ( + + + {title} + {desc} + + {children} + + ); +} + +export function ModelLeaderboard({ models }: { models: ModelRow[] }) { + return ( + + [ + {i + 1}, + + {m.model_name} + {m.engine_type ?? ""}{m.provider_name ? ` · ${m.provider_name}` : ""} + , + formatCompact(m.chats), + formatCompact(m.total_tokens), + formatMs(m.avg_latency_ms), + m.failed > 0 ? {m.failed} : "0", + ])} + /> + + ); +} + +export function DeviceLeaderboard({ rows }: { rows: DeviceUsageRow[] }) { + return ( + +
[ + + {d.device_name ?? "Unknown device"} + {d.platform} + , + v{d.app_version ?? "?"}, + formatCompact(d.chats), + formatCompact(d.total_tokens), + formatCompact(d.sessions), + ])} + /> + + ); +} + +export function SessionLeaderboard({ sessions }: { sessions: ActivitySession[] }) { + const ranked = [...sessions] + .map((s) => ({ + s, + secs: s.ended_at ? Math.max(0, (new Date(s.ended_at).getTime() - new Date(s.started_at).getTime()) / 1000) : null, + })) + .sort((a, b) => (b.secs ?? -1) - (a.secs ?? -1)) + .slice(0, 10); + return ( + +
[ + {new Date(s.started_at).toLocaleDateString("en-US", { month: "short", day: "numeric" })}, + s.engine_type ?? "—", + formatCompact(s.events), + secs === null ? Active : formatDuration(secs), + ])} + /> + + ); +} + +export function ErrorLeaderboard({ events }: { events: ActivityEvent[] }) { + const byCode = new Map(); + for (const e of events) { + if (e.success) continue; + const code = e.error_code ?? "unknown"; + const cur = byCode.get(code) ?? { count: 0, model: e.model_name ?? "—" }; + cur.count++; + byCode.set(code, cur); + } + const rows = [...byCode.entries()].sort((a, b) => b[1].count - a[1].count).slice(0, 10); + return ( + +
[ + {code}, + formatCompact(v.count), + v.model, + ])} + /> + + ); +} diff --git a/website/components/layout/navbar.tsx b/website/components/layout/navbar.tsx index bca8d87e..313d1d4e 100644 --- a/website/components/layout/navbar.tsx +++ b/website/components/layout/navbar.tsx @@ -257,6 +257,15 @@ export function Navbar() { Docs + + + + Dashboard + + diff --git a/website/components/ui/sign-up.test.tsx b/website/components/ui/sign-up.test.tsx new file mode 100644 index 00000000..13088d97 --- /dev/null +++ b/website/components/ui/sign-up.test.tsx @@ -0,0 +1,49 @@ +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { AuthComponent } from "./sign-up"; +import { signInWithGitHub, signInWithGoogle } from "@/lib/firebase-client"; + +vi.mock("@/lib/firebase-client", () => ({ + signInWithGoogle: vi.fn(), + signInWithGitHub: vi.fn(), +})); + +afterEach(() => cleanup()); + +const googleMock = vi.mocked(signInWithGoogle); +const githubMock = vi.mocked(signInWithGitHub); + +describe("AuthComponent (OAuth only)", () => { + it("offers Google and GitHub with no manual email option", () => { + render( {}} />); + expect(screen.getByRole("button", { name: /google/i })).toBeDefined(); + expect(screen.getByRole("button", { name: /github/i })).toBeDefined(); + expect(screen.queryByPlaceholderText(/email/i)).toBeNull(); + expect(screen.queryByPlaceholderText(/password/i)).toBeNull(); + expect(screen.queryByRole("textbox")).toBeNull(); + }); + + it("celebrates and redirects after Google sign-in", async () => { + googleMock.mockResolvedValueOnce({ email: "a@example.com" } as never); + const onAuthenticated = vi.fn(); + render(); + + fireEvent.click(screen.getByRole("button", { name: /google/i })); + + await waitFor(() => expect(screen.getByText(/taking you to your dashboard/i)).toBeDefined(), { + timeout: 3000, + }); + await waitFor(() => expect(onAuthenticated).toHaveBeenCalledTimes(1), { timeout: 3000 }); + }); + + it("shows a friendly error when GitHub sign-in fails", async () => { + githubMock.mockRejectedValueOnce(Object.assign(new Error("popup closed"), { code: "auth/popup-closed-by-user" })); + const onAuthenticated = vi.fn(); + render(); + + fireEvent.click(screen.getByRole("button", { name: /github/i })); + + await waitFor(() => expect(screen.getByText(/window was closed/i)).toBeDefined(), { timeout: 3000 }); + expect(onAuthenticated).not.toHaveBeenCalled(); + }); +}); diff --git a/website/components/ui/sign-up.tsx b/website/components/ui/sign-up.tsx new file mode 100644 index 00000000..01debdfb --- /dev/null +++ b/website/components/ui/sign-up.tsx @@ -0,0 +1,506 @@ +"use client"; + +/** + * OAuth-only sign-in/sign-up card (Google + GitHub via Firebase). + * + * Adapted to this repo: no email/password flow (product decision — the + * Android app offers exactly Google + GitHub), themed to the site's dark + * palette instead of stock shadcn tokens, and wired to + * `@/lib/firebase-client`. Visual system kept from the original: glass + * buttons, blur-fade entrances, rotating hero words, gradient backdrop, + * and a confetti burst on success. + */ +import { cn } from "@/lib/utils"; +import { + Children, + createContext, + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from "react"; +import type { ComponentPropsWithRef, ReactNode } from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { AlertCircle, Gem, Loader, PartyPopper } from "lucide-react"; +import { + AnimatePresence, + motion, + useInView, + type Transition, + type Variants, +} from "framer-motion"; +import type { + GlobalOptions as ConfettiGlobalOptions, + CreateTypes as ConfettiInstance, + Options as ConfettiOptions, +} from "canvas-confetti"; +import confetti from "canvas-confetti"; +import { signInWithGitHub, signInWithGoogle } from "@/lib/firebase-client"; + +// --- CONFETTI --- +type ConfettiApi = { fire: (options?: ConfettiOptions) => void }; +export type ConfettiRef = ConfettiApi | null; +const ConfettiContext = createContext({} as ConfettiApi); +void ConfettiContext; + +const Confetti = forwardRef< + ConfettiRef, + ComponentPropsWithRef<"canvas"> & { + options?: ConfettiOptions; + globalOptions?: ConfettiGlobalOptions; + manualstart?: boolean; + } +>((props, ref) => { + const { options, globalOptions = { resize: true, useWorker: true }, manualstart = false, ...rest } = props; + const instanceRef = useRef(null); + const canvasRef = useCallback( + (node: HTMLCanvasElement | null) => { + if (node !== null) { + if (instanceRef.current) return; + try { + instanceRef.current = confetti.create(node, { ...globalOptions, resize: true }); + } catch { + instanceRef.current = null; + } + } else if (instanceRef.current) { + try { + instanceRef.current.reset(); + } catch { + /* noop */ + } + instanceRef.current = null; + } + }, + [globalOptions], + ); + const fire = useCallback( + (opts: ConfettiOptions = {}) => { + try { + instanceRef.current?.({ ...options, ...opts }); + } catch { + /* canvas-confetti needs a real 2d context; never break auth for it */ + } + }, + [options], + ); + const api = useMemo(() => ({ fire }), [fire]); + useImperativeHandle(ref, () => api, [api]); + useEffect(() => { + if (!manualstart) fire(); + }, [manualstart, fire]); + return ; +}); +Confetti.displayName = "Confetti"; + +// --- TEXT LOOP --- +type TextLoopProps = { + children: ReactNode[]; + className?: string; + interval?: number; + transition?: Transition; + variants?: Variants; + onIndexChange?: (index: number) => void; +}; +export function TextLoop({ + children, + className, + interval = 2, + transition = { duration: 0.3 }, + variants, + onIndexChange, +}: TextLoopProps) { + const [currentIndex, setCurrentIndex] = useState(0); + const items = Children.toArray(children); + useEffect(() => { + if (items.length < 2) return; + const timer = setInterval(() => { + setCurrentIndex((current) => { + const next = (current + 1) % items.length; + onIndexChange?.(next); + return next; + }); + }, interval * 1000); + return () => clearInterval(timer); + }, [items.length, interval, onIndexChange]); + const motionVariants: Variants = { + initial: { y: 20, opacity: 0 }, + animate: { y: 0, opacity: 1 }, + exit: { y: -20, opacity: 0 }, + }; + return ( +
+ + + {items[currentIndex]} + + +
+ ); +} + +// --- BLUR FADE --- +type BlurFadeProps = { + children: ReactNode; + className?: string; + duration?: number; + delay?: number; + yOffset?: number; + inView?: boolean; + blur?: string; +}; +function BlurFade({ + children, + className, + duration = 0.4, + delay = 0, + yOffset = 6, + inView = true, + blur = "6px", +}: BlurFadeProps) { + const ref = useRef(null); + const inViewResult = useInView(ref, { once: true }); + const isInView = !inView || inViewResult; + const defaultVariants: Variants = { + hidden: { y: yOffset, opacity: 0, filter: `blur(${blur})` }, + visible: { y: -yOffset, opacity: 1, filter: "blur(0px)" }, + }; + return ( + + {children} + + ); +} + +// --- GLASS BUTTON --- +const glassButtonVariants = cva("relative isolate cursor-pointer rounded-full transition-all", { + variants: { + size: { + default: "text-base font-medium", + sm: "text-sm font-medium", + lg: "text-lg font-medium", + icon: "h-10 w-10", + }, + }, + defaultVariants: { size: "default" }, +}); +const glassButtonTextVariants = cva("glass-button-text relative block select-none tracking-tighter", { + variants: { + size: { + default: "px-6 py-3.5", + sm: "px-4 py-2", + lg: "px-8 py-4", + icon: "flex h-10 w-10 items-center justify-center", + }, + }, + defaultVariants: { size: "default" }, +}); +export interface GlassButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + contentClassName?: string; +} +const GlassButton = forwardRef( + ({ className, children, size, contentClassName, onClick, ...props }, ref) => { + const handleWrapperClick = (e: React.MouseEvent) => { + const button = e.currentTarget.querySelector("button"); + if (button && e.target !== button) button.click(); + }; + return ( +
+ +
+
+ ); + }, +); +GlassButton.displayName = "GlassButton"; + +// --- GRADIENT BACKDROP (site palette) --- +const GradientBackground = () => ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +// --- PROVIDER ICONS --- +const GoogleIcon = (props: React.SVGProps) => ( + + + + + + + + + + +); +const GitHubIcon = (props: React.SVGProps) => ( + + + +); + +const DefaultLogo = () => ( +
+ +
+); + +// --- MAIN COMPONENT --- +type AuthStatus = "idle" | "working" | "success" | "error"; + +interface AuthComponentProps { + logo?: ReactNode; + brandName?: string; + /** Where to send the user after a successful sign-in. Defaults to /dashboard. */ + redirectTo?: string; + /** Overrides the default router push (used by tests). */ + onAuthenticated?: () => void; +} + +function friendlyAuthError(e: unknown): string { + const code = typeof e === "object" && e !== null ? (e as { code?: unknown }).code : undefined; + if (code === "auth/popup-closed-by-user") return "Sign-in window was closed — try again."; + if (code === "auth/cancelled-popup-request") return "Another sign-in is already open."; + if (code === "auth/unauthorized-domain") return "This domain isn't authorized in Firebase console."; + if (code === "auth/network-request-failed") return "Network error — check your connection and retry."; + if (code === "auth/account-exists-with-different-credential") + return "This email is already linked to another provider — use that one."; + if (e instanceof Error && e.message) return e.message; + return "Sign-in failed — please try again."; +} + +export const AuthComponent = ({ + logo = , + brandName = "AndroLLM", + redirectTo = "/dashboard", + onAuthenticated, +}: AuthComponentProps) => { + const [status, setStatus] = useState("idle"); + const [provider, setProvider] = useState<"google" | "github" | null>(null); + const [errorMessage, setErrorMessage] = useState(""); + const confettiRef = useRef(null); + + const celebrate = () => { + const fire = confettiRef.current?.fire; + if (!fire) return; + const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 100 }; + const particleCount = 50; + fire({ ...defaults, particleCount, origin: { x: 0, y: 1 }, angle: 60 }); + fire({ ...defaults, particleCount, origin: { x: 1, y: 1 }, angle: 120 }); + }; + + const finishSuccess = () => { + setStatus("success"); + celebrate(); + window.setTimeout(() => { + if (onAuthenticated) onAuthenticated(); + else window.location.assign(redirectTo); + }, 1400); + }; + + const signIn = async (which: "google" | "github") => { + if (status === "working") return; + setProvider(which); + setErrorMessage(""); + setStatus("working"); + try { + if (which === "google") await signInWithGoogle(); + else await signInWithGitHub(); + finishSuccess(); + } catch (e) { + setErrorMessage(friendlyAuthError(e)); + setStatus("error"); + } + }; + + const reset = () => { + setStatus("idle"); + setProvider(null); + setErrorMessage(""); + }; + + return ( +
+ + + + +
+ +
+
+
+ + + {logo} + {brandName} + + + +
+

+ Welcome back +

+
+
+ + + Private AI on your device + Your models, your dashboard + Sign in to continue + + + + + void signIn("google")} + disabled={status === "working"} + aria-label="Continue with Google" + > + {status === "working" && provider === "google" ? ( + + ) : ( + + )} + Google + + void signIn("github")} + disabled={status === "working"} + aria-label="Continue with GitHub" + > + {status === "working" && provider === "github" ? ( + + ) : ( + + )} + GitHub + + + +
+ {status === "working" && ( +

+ + {provider === "google" ? "Waiting for Google…" : "Waiting for GitHub…"} +

+ )} + {status === "success" && ( +

+ + Signed in — taking you to your dashboard… +

+ )} + {status === "error" && ( + <> +

+ + {errorMessage} +

+ + + )} + {status === "idle" && ( +

+ Same account as your Android app. +
+ No email sign-in — Google or GitHub only. +

+ )} +
+
+
+ ); +}; diff --git a/website/hooks/use-backend-user.ts b/website/hooks/use-backend-user.ts new file mode 100644 index 00000000..9c58bbb6 --- /dev/null +++ b/website/hooks/use-backend-user.ts @@ -0,0 +1,70 @@ +"use client"; + +/** + * Phase 1 — React hook bridging Firebase web auth and the backend profile. + * + * Flow: Firebase sign-in (same project as Android) → Firebase ID token → + * backend `/auth/session` → shared UserProfile. Returns the Firebase user + * plus the backend profile so Phase 4 can gate analytics on + * `profile.web_connected`. + */ +import { useCallback, useEffect, useState } from "react"; +import { + subscribeToAuthChanges, + type User as FirebaseUser, +} from "@/lib/firebase-client"; +import { + BackendError, + fetchSession, + type UserProfile, +} from "@/lib/backend-client"; + +export type BackendUserState = { + firebaseUser: FirebaseUser | null; + profile: UserProfile | null; + loading: boolean; + error: string | null; + refresh: () => Promise; +}; + +export function useBackendUser(): BackendUserState { + const [firebaseUser, setFirebaseUser] = useState(null); + const [profile, setProfile] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const refresh = useCallback(async () => { + setLoading(true); + setError(null); + try { + const session = await fetchSession(); + setProfile(session.user); + } catch (e) { + setProfile(null); + if (e instanceof BackendError && e.status === 401) { + // Signed out or backend has no row yet — not fatal for Phase 1. + setError(null); + } else { + setError(e instanceof Error ? e.message : "Failed to load backend profile."); + } + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + const unsubscribe = subscribeToAuthChanges((user) => { + setFirebaseUser(user); + if (!user) { + setProfile(null); + setError(null); + setLoading(false); + return; + } + void refresh(); + }); + return unsubscribe; + }, [refresh]); + + return { firebaseUser, profile, loading, error, refresh }; +} diff --git a/website/hooks/use-dashboard.ts b/website/hooks/use-dashboard.ts new file mode 100644 index 00000000..049fa50c --- /dev/null +++ b/website/hooks/use-dashboard.ts @@ -0,0 +1,128 @@ +"use client"; + +/** + * Phase 4 — dashboard data orchestration. + * + * States: signed-out | not-connected (profile says so, or backend 403s) | + * loading | error | empty (connected, zero usage) | ready. + * + * Data freshness: the app uploads ~8s after each generation (debounced + * trigger), and this hook silently re-fetches every 10s while the tab is + * visible — new usage appears on screen within ~10s of happening. + */ +import { useCallback, useEffect, useRef, useState } from "react"; +import { BackendError } from "@/lib/backend-client"; +import { + fetchDashboardActivity, + fetchDashboardCharts, + fetchDashboardSummary, + isNotConnectedError, + type DashboardActivity, + type DashboardCharts, + type DashboardSummary, +} from "@/lib/dashboard-client"; +import { isEmptyTotals } from "@/lib/dashboard-format"; +import { useBackendUser } from "./use-backend-user"; + +export type DashboardStatus = + | "signed-out" + | "not-connected" + | "loading" + | "error" + | "empty" + | "ready"; + +export type DashboardState = { + status: DashboardStatus; + summary: DashboardSummary | null; + charts: DashboardCharts | null; + activity: DashboardActivity | null; + error: string | null; + refresh: () => Promise; +}; + +export function useDashboard(): DashboardState { + const { firebaseUser, profile, loading: userLoading } = useBackendUser(); + const [summary, setSummary] = useState(null); + const [charts, setCharts] = useState(null); + const [activity, setActivity] = useState(null); + const [status, setStatus] = useState("loading"); + const [error, setError] = useState(null); + const statusRef = useRef(status); + statusRef.current = status; + + // Always a 90-day window: ranges slice client-side, and growth/streaks + // compare against the previous period inside the same payload. + const days = 90; + + const fetchAll = useCallback(async () => { + const [s, c, a] = await Promise.all([ + fetchDashboardSummary(), + fetchDashboardCharts(days), + fetchDashboardActivity(20), + ]); + return { s, c, a }; + }, [days]); + + const refresh = useCallback(async () => { + setStatus("loading"); + setError(null); + try { + const { s, c, a } = await fetchAll(); + setSummary(s); + setCharts(c); + setActivity(a); + setStatus(isEmptyTotals(s.totals) ? "empty" : "ready"); + } catch (e) { + if (isNotConnectedError(e)) { + setStatus("not-connected"); + } else if (e instanceof BackendError && e.status === 401) { + setError("Session expired — please sign in again."); + setStatus("error"); + } else { + setError(e instanceof Error ? e.message : "Failed to load dashboard."); + setStatus("error"); + } + } + }, [days, fetchAll]); + + useEffect(() => { + if (userLoading) { + setStatus("loading"); + return; + } + if (!firebaseUser) { + setStatus("signed-out"); + return; + } + if (profile && !profile.web_connected) { + setStatus("not-connected"); + return; + } + if (profile?.web_connected) { + void refresh(); + } + }, [userLoading, firebaseUser, profile, refresh]); + + // Silent 10s poll while data is on screen and the tab is visible — shows + // freshly synced usage without a loading flash. Errors never disturb the + // current view (the manual Refresh button surfaces them). + useEffect(() => { + if (typeof document === "undefined") return; + const id = window.setInterval(() => { + if (document.visibilityState !== "visible") return; + if (statusRef.current !== "ready" && statusRef.current !== "empty") return; + fetchAll() + .then(({ s, c, a }) => { + setSummary(s); + setCharts(c); + setActivity(a); + setStatus(isEmptyTotals(s.totals) ? "empty" : "ready"); + }) + .catch(() => {}); + }, 10_000); + return () => window.clearInterval(id); + }, [fetchAll]); + + return { status, summary, charts, activity, error, refresh }; +} diff --git a/website/lib/backend-client.ts b/website/lib/backend-client.ts new file mode 100644 index 00000000..3fccd0f3 --- /dev/null +++ b/website/lib/backend-client.ts @@ -0,0 +1,102 @@ +/** + * Phase 1 — typed client for the private backend identity endpoints. + * + * Base URL: NEXT_PUBLIC_BACKEND_URL (e.g. https://api.androllm.app). + * Auth: `Authorization: Bearer ` (see firebase-client.ts). + * Identity always comes from the verified token server-side — this client + * never sends a user id. + * + * Endpoints: POST /auth/verify, GET /auth/session, GET /me. + * Phase 1 only — no analytics/sync calls live here. + */ +import { getIdToken } from "./firebase-client"; + +export type UserProfile = { + id: string; + firebase_uid: string; + email: string | null; + display_name: string | null; + photo_url: string | null; + created_at: string; + last_seen_at: string; + sync_enabled: boolean; + settings: Record; + web_connected: boolean; + web_connected_at: string | null; +}; + +export type AuthSession = { + authenticated: boolean; + user: UserProfile; +}; + +export class BackendError extends Error { + status: number; + constructor(status: number, message: string) { + super(message); + this.status = status; + } +} + +function baseUrl(): string { + return (process.env.NEXT_PUBLIC_BACKEND_URL ?? "").trim().replace(/\/+$/, ""); +} + +export function isBackendConfigured(): boolean { + return baseUrl().length > 0; +} + +async function authedFetch(path: string, init?: RequestInit): Promise { + const base = baseUrl(); + if (!base) throw new BackendError(0, "Backend is not configured (missing NEXT_PUBLIC_BACKEND_URL)."); + const token = await getIdToken(); + if (!token) throw new BackendError(401, "Not signed in (no Firebase ID token)."); + return fetch(`${base}${path}`, { + ...init, + headers: { + "Content-Type": "application/json", + ...(init?.headers ?? {}), + Authorization: `Bearer ${token}`, + }, + }); +} + +async function parseOrThrow(res: Response): Promise { + if (res.status === 401) throw new BackendError(401, "Unauthorized (invalid or expired session)."); + if (!res.ok) throw new BackendError(res.status, `Backend request failed: HTTP ${res.status}`); + return (await res.json()) as T; +} + +/** Authenticated GET helper for Phase 4+ feature clients (token attached, errors typed). */ +export async function apiGet(path: string): Promise { + const res = await authedFetch(path, { method: "GET" }); + return parseOrThrow(res); +} + +/** Verify session with backend; creates/updates the profile server-side. */ +export async function verifySession(opts?: { + displayName?: string; + photoUrl?: string; +}): Promise { + const res = await authedFetch("/auth/verify", { + method: "POST", + body: JSON.stringify({ + display_name: opts?.displayName ?? undefined, + photo_url: opts?.photoUrl ?? undefined, + device: { platform: "web", app_version: undefined }, + }), + }); + return parseOrThrow(res); +} + +/** Lightweight session check for website startup. */ +export async function fetchSession(): Promise { + const res = await authedFetch("/auth/session", { method: "GET" }); + return parseOrThrow(res); +} + +/** Current backend profile for the verified UID (Phase 4 dashboard gates on this). */ +export async function fetchMe(): Promise { + const res = await authedFetch("/me", { method: "GET" }); + return parseOrThrow(res); +} diff --git a/website/lib/dashboard-client.test.ts b/website/lib/dashboard-client.test.ts new file mode 100644 index 00000000..97a4b234 --- /dev/null +++ b/website/lib/dashboard-client.test.ts @@ -0,0 +1,62 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { BackendError } from "./backend-client"; +import { + fetchDashboardActivity, + fetchDashboardCharts, + fetchDashboardSummary, + isNotConnectedError, +} from "./dashboard-client"; +import { getIdToken } from "./firebase-client"; + +vi.mock("./firebase-client", () => ({ + getIdToken: vi.fn(), +})); + +const tokenMock = vi.mocked(getIdToken); + +function jsonResponse(body: unknown, status = 200) { + return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } }); +} + +describe("dashboard client auth", () => { + beforeEach(() => { + vi.unstubAllGlobals(); + vi.stubGlobal("fetch", vi.fn()); + process.env.NEXT_PUBLIC_BACKEND_URL = "https://api.example.test"; + tokenMock.mockResolvedValue("test-id-token"); + }); + + it("attaches the Firebase ID token to every dashboard call", async () => { + const fetchMock = vi.mocked(fetch); + fetchMock.mockResolvedValueOnce(jsonResponse({ totals: {} })); + fetchMock.mockResolvedValueOnce(jsonResponse({ per_day: [], models: [] })); + fetchMock.mockResolvedValueOnce(jsonResponse({ events: [], sessions: [], devices: [] })); + + await fetchDashboardSummary(); + await fetchDashboardCharts(30); + await fetchDashboardActivity(20); + + expect(fetchMock).toHaveBeenCalledTimes(3); + for (const call of fetchMock.mock.calls) { + const [url, init] = call as [string, RequestInit]; + expect(url.startsWith("https://api.example.test/dashboard/")).toBe(true); + expect((init.headers as Record).Authorization).toBe("Bearer test-id-token"); + } + }); + + it("maps 403 to the not-connected signal", async () => { + vi.mocked(fetch).mockResolvedValueOnce(jsonResponse({ error: "web_not_connected" }, 403)); + const err = await fetchDashboardSummary().catch((e) => e); + expect(err).toBeInstanceOf(BackendError); + expect((err as BackendError).status).toBe(403); + expect(isNotConnectedError(err)).toBe(true); + expect(isNotConnectedError(new Error("nope"))).toBe(false); + }); + + it("surfaces auth failures clearly", async () => { + vi.mocked(fetch).mockResolvedValueOnce(jsonResponse({ error: "unauthorized" }, 401)); + const err = await fetchDashboardCharts().catch((e) => e); + expect(err).toBeInstanceOf(BackendError); + expect(isNotConnectedError(err)).toBe(false); + }); +}); diff --git a/website/lib/dashboard-client.ts b/website/lib/dashboard-client.ts new file mode 100644 index 00000000..14270c42 --- /dev/null +++ b/website/lib/dashboard-client.ts @@ -0,0 +1,157 @@ +/** + * Phase 4 — typed client for the personal dashboard endpoints. + * + * Same auth model as backend-client.ts: Firebase ID token attached per + * request, identity derived server-side from the verified token. These + * endpoints return ONLY the signed-in user's rows (403 web_not_connected + * when the Android app was never linked). + */ +import { apiGet, BackendError } from "./backend-client"; + +export type SummaryTotals = { + chats: number; + events: number; + total_tokens: number; + input_tokens: number; + output_tokens: number; + local_tokens: number; + cloud_tokens: number; + sessions: number; + avg_latency_ms: number | null; + avg_ttft_ms: number | null; + failed: number; + devices: number; + models_count: number; + longest_session_seconds: number | null; + peak_day: { day: string; chats: number } | null; + active_days: number; + last_event_at: string | null; + last_synced_at: string | null; + by_source: { source_page: string | null; events: number }[]; +}; + +export type DayRow = { + day: string; + chats: number; + events: number; + input_tokens: number; + output_tokens: number; + total_tokens: number; + local_tokens: number; + cloud_tokens: number; + local_chats: number; + cloud_chats: number; + successes: number; + sessions: number; + errors: number; + avg_latency_ms: number | null; + avg_ttft_ms: number | null; +}; + +export type ModelRow = { + model_name: string; + provider_name: string | null; + engine_type: string | null; + chats: number; + events: number; + total_tokens: number; + avg_latency_ms: number | null; + failed: number; +}; + +export type ActivityEvent = { + event_id: string; + event_type: string; + model_name: string | null; + provider_name: string | null; + engine_type: string | null; + total_tokens: number; + latency_ms: number | null; + success: boolean; + error_code: string | null; + device_name: string | null; + created_at: string; +}; + +export type ActivitySession = { + id: string; + started_at: string; + ended_at: string | null; + engine_type: string | null; + device_name: string | null; + events: number; +}; + +export type ActivityDevice = { + id: string; + device_identifier: string; + device_name: string | null; + platform: string; + app_version: string | null; + last_seen_at: string; +}; + +export type DashboardSummary = { totals: SummaryTotals }; + +export type HeatCell = { dow: number; hour: number; chats: number; tokens: number }; + +export type ProviderRow = { + provider_name: string; + chats: number; + events: number; + total_tokens: number; + avg_latency_ms: number | null; + failed: number; +}; + +export type DeviceUsageRow = { + device_name: string | null; + app_version: string | null; + platform: string; + chats: number; + events: number; + total_tokens: number; + sessions: number; +}; + +export type AppVersionRow = { + app_version: string; + devices: number; + chats: number; + total_tokens: number; +}; + +export type BucketCount = { bucket: string; count: number }; + +export type DashboardCharts = { + per_day: DayRow[]; + models: ModelRow[]; + heatmap: HeatCell[]; + providers: ProviderRow[]; + by_device: DeviceUsageRow[]; + app_versions: AppVersionRow[]; + token_histogram: BucketCount[]; + session_durations: BucketCount[]; +}; +export type DashboardActivity = { + events: ActivityEvent[]; + sessions: ActivitySession[]; + devices: ActivityDevice[]; +}; + +/** True when the backend says this account never linked the Android app. */ +export function isNotConnectedError(e: unknown): boolean { + return e instanceof BackendError && e.status === 403; +} + +export async function fetchDashboardSummary(): Promise { + return apiGet("/dashboard/summary"); +} + +export async function fetchDashboardCharts(days = 30): Promise { + return apiGet(`/dashboard/charts?days=${days}`); +} + +export async function fetchDashboardActivity(limit = 20): Promise { + return apiGet(`/dashboard/activity?limit=${limit}`); +} diff --git a/website/lib/dashboard-format.test.ts b/website/lib/dashboard-format.test.ts new file mode 100644 index 00000000..ebbfdae9 --- /dev/null +++ b/website/lib/dashboard-format.test.ts @@ -0,0 +1,143 @@ +import { describe, expect, it } from "vitest"; +import { + currentStreak, + dayLabel, + dowName, + engineSplit, + failureRate, + formatCompact, + formatDuration, + formatGrowth, + formatMs, + formatPercent, + groupByWeek, + growthRate, + hourLabel, + isEmptyTotals, + orderBuckets, + peakSplit, + relativeTime, + sliceRange, + sumDays, + timeLabel, + weekdaySplit, +} from "./dashboard-format"; + +describe("formatCompact", () => { + it("passes small numbers through", () => { + expect(formatCompact(0)).toBe("0"); + expect(formatCompact(950)).toBe("950"); + }); + it("compacts thousands and millions", () => { + expect(formatCompact(1500)).toBe("1.5K"); + expect(formatCompact(2000)).toBe("2K"); + expect(formatCompact(2_500_000)).toBe("2.5M"); + }); + it("handles non-finite input", () => { + expect(formatCompact(NaN)).toBe("—"); + }); +}); + +describe("formatMs", () => { + it("formats ms, seconds, minutes", () => { + expect(formatMs(800)).toBe("800 ms"); + expect(formatMs(2500)).toBe("2.5 s"); + expect(formatMs(90_000)).toBe("1.5 min"); + }); + it("is null-safe", () => { + expect(formatMs(null)).toBe("—"); + expect(formatMs(undefined)).toBe("—"); + }); +}); + +describe("labels", () => { + it("formats day labels", () => { + expect(dayLabel("2026-09-21")).toBe("Sep 21"); + expect(dayLabel("junk")).toBe("junk"); + }); + it("formats timestamps with date and time", () => { + expect(timeLabel("2026-09-21T13:25:00.000Z")).toMatch(/Sep 21/); + expect(timeLabel("junk")).toBe("junk"); + }); +}); + +describe("failureRate", () => { + it("computes the share and guards empty totals", () => { + expect(failureRate(1, 4)).toBeCloseTo(0.25); + expect(failureRate(0, 0)).toBeNull(); + expect(formatPercent(failureRate(1, 4))).toBe("25%"); + expect(formatPercent(null)).toBe("—"); + }); +}); + +describe("isEmptyTotals", () => { + it("detects connected-but-unused accounts", () => { + expect(isEmptyTotals({ chats: 0, events: 0, total_tokens: 0, sessions: 0 })).toBe(true); + expect(isEmptyTotals({ chats: 1, events: 1, total_tokens: 10, sessions: 0 })).toBe(false); + }); +}); + +describe("engineSplit", () => { + it("omits empty slices so the donut never renders zeros", () => { + expect(engineSplit(0, 0)).toEqual([]); + expect(engineSplit(600, 0)).toEqual([{ name: "Local", value: 600 }]); + expect(engineSplit(600, 200)).toHaveLength(2); + }); +}); + +describe("ranges and growth", () => { + const rows = [ + { day: "2026-09-18", chats: 1, total_tokens: 100, sessions: 1, errors: 0 }, + { day: "2026-09-19", chats: 2, total_tokens: 200, sessions: 1, errors: 0 }, + { day: "2026-09-20", chats: 3, total_tokens: 300, sessions: 2, errors: 1 }, + { day: "2026-09-21", chats: 4, total_tokens: 400, sessions: 2, errors: 0 }, + ]; + it("slices ranges and sums", () => { + expect(sliceRange(rows, 7)).toHaveLength(4); + expect(sumDays(rows, (r) => r.chats)).toBe(10); + }); + it("computes growth safely", () => { + expect(growthRate(10, 5)).toBeCloseTo(1); + expect(growthRate(5, 0)).toBeNull(); + expect(formatGrowth(0.25)).toEqual({ text: "+25.0%", up: true }); + expect(formatGrowth(null)).toEqual({ text: "—", up: null }); + }); + it("groups weeks and streaks", () => { + expect(groupByWeek(rows).length).toBeGreaterThan(0); + expect(currentStreak(rows, "2026-09-21")).toBe(4); + expect(currentStreak([{ day: "2026-09-19", chats: 1 }], "2026-09-21")).toBe(0); + }); +}); + +describe("heatmap helpers", () => { + it("names days and hours", () => { + expect(dowName(0)).toBe("Sun"); + expect(dowName(1)).toBe("Mon"); + expect(hourLabel(0)).toBe("12a"); + expect(hourLabel(13)).toBe("1p"); + }); + it("splits weekday/weekend and peak/off-hours", () => { + const cells = [ + { dow: 1, hour: 9, chats: 10, tokens: 100 }, + { dow: 6, hour: 10, chats: 4, tokens: 40 }, + ]; + const { weekday, weekend } = weekdaySplit(cells); + expect(weekday.chats).toBe(10); + expect(weekend.chats).toBe(4); + const { peak, off } = peakSplit(cells); + expect(peak.chats + off.chats).toBe(14); + }); + it("orders buckets canonically", () => { + expect(orderBuckets([{ bucket: "1k-2k", count: 2 }], ["0-100", "1k-2k"]).map((b) => b.count)).toEqual([0, 2]); + }); +}); + +describe("freshness helpers", () => { + it("formats durations and relative times", () => { + expect(formatDuration(45)).toBe("45s"); + expect(formatDuration(150)).toBe("2.5m"); + expect(formatDuration(null)).toBe("—"); + expect(relativeTime(new Date(Date.now() - 5 * 60_000).toISOString())).toBe("5m ago"); + expect(relativeTime(null)).toBe("—"); + }); +}); diff --git a/website/lib/dashboard-format.ts b/website/lib/dashboard-format.ts new file mode 100644 index 00000000..4fe79d45 --- /dev/null +++ b/website/lib/dashboard-format.ts @@ -0,0 +1,218 @@ +/** + * Phase 4 — pure presentation helpers for the dashboard (unit-tested). + * No Firebase, no fetch, no backend shapes beyond plain numbers. + */ + +/** Compact number: 950 → "950", 1500 → "1.5K", 2_500_000 → "2.5M". */ +export function formatCompact(n: number): string { + if (!Number.isFinite(n)) return "—"; + const abs = Math.abs(n); + if (abs >= 1_000_000_000) return `${trim(n / 1_000_000_000)}B`; + if (abs >= 1_000_000) return `${trim(n / 1_000_000)}M`; + if (abs >= 1_000) return `${trim(n / 1_000)}K`; + return String(Math.round(n)); +} + +function trim(v: number): string { + const rounded = Math.round(v * 10) / 10; + return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1); +} + +/** Milliseconds → "800 ms", "2.5 s", "1.2 min". Null-safe. */ +export function formatMs(ms: number | null | undefined): string { + if (ms === null || ms === undefined || !Number.isFinite(ms)) return "—"; + if (ms < 1000) return `${Math.round(ms)} ms`; + if (ms < 60_000) return `${trim(ms / 1000)} s`; + return `${trim(ms / 60_000)} min`; +} + +/** "2026-09-21" → "Sep 21". Falls back to the raw string. */ +export function dayLabel(isoDay: string): string { + const d = new Date(`${isoDay}T00:00:00Z`); + if (Number.isNaN(d.getTime())) return isoDay; + return d.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" }); +} + +/** ISO timestamp → "Sep 21, 14:32" (local tz). Falls back to raw string. */ +export function timeLabel(iso: string): string { + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return iso; + return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }) + + ", " + + d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }); +} + +/** Failure share 0..1, null when there is nothing to divide by. */ +export function failureRate(failed: number, total: number): number | null { + if (!Number.isFinite(failed) || !Number.isFinite(total) || total <= 0) return null; + return Math.min(1, Math.max(0, failed / total)); +} + +export function formatPercent(ratio: number | null): string { + if (ratio === null) return "—"; + return `${(ratio * 100).toFixed(ratio < 0.1 && ratio > 0 ? 1 : 0)}%`; +} + +/** True when every headline total is zero (connected but nothing synced yet). */ +export function isEmptyTotals(t: { chats: number; events: number; total_tokens: number; sessions: number }): boolean { + return t.chats === 0 && t.events === 0 && t.total_tokens === 0 && t.sessions === 0; +} + +/** Local vs cloud token split for the donut (omits empty slices). */ +export function engineSplit(localTokens: number, cloudTokens: number): { name: string; value: number }[] { + const out: { name: string; value: number }[] = []; + if (localTokens > 0) out.push({ name: "Local", value: localTokens }); + if (cloudTokens > 0) out.push({ name: "Cloud", value: cloudTokens }); + return out; +} + +/** Seconds → "45s", "12m", "3.2h". Null-safe. */ +export function formatDuration(seconds: number | null | undefined): string { + if (seconds === null || seconds === undefined || !Number.isFinite(seconds)) return "—"; + if (seconds < 60) return `${Math.round(seconds)}s`; + if (seconds < 3600) return `${trim(seconds / 60)}m`; + return `${trim(seconds / 3600)}h`; +} + +/** ISO timestamp → "2h ago", "3d ago", "just now". Falls back to "—". */ +export function relativeTime(iso: string | null | undefined, nowMs = Date.now()): string { + if (!iso) return "—"; + const t = new Date(iso).getTime(); + if (Number.isNaN(t)) return "—"; + const diff = Math.max(0, nowMs - t); + const mins = Math.floor(diff / 60_000); + if (mins < 1) return "just now"; + if (mins < 60) return `${mins}m ago`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + if (days < 30) return `${days}d ago`; + return `${Math.floor(days / 30)}mo ago`; +} + +export type RangeKey = 7 | 30 | 90; + +/** Last N rows of a day series (oldest first). */ +export function sliceRange(rows: T[], range: RangeKey): T[] { + return rows.slice(Math.max(0, rows.length - range)); +} + +export function sumDays(rows: T[], pick: (r: T) => number): number { + return rows.reduce((n, r) => n + pick(r), 0); +} + +export type WeekBucket = { + label: string; + chats: number; + tokens: number; + sessions: number; + errors: number; +}; + +/** Group a day series into ISO-ish calendar weeks (oldest first). */ +export function groupByWeek( + rows: T[], +): WeekBucket[] { + const weeks: WeekBucket[] = []; + let curKey = ""; + for (const r of rows) { + const d = new Date(`${r.day}T00:00:00Z`); + if (Number.isNaN(d.getTime())) continue; + const monday = new Date(d); + monday.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 6) % 7)); + const key = monday.toISOString().slice(0, 10); + if (key !== curKey || weeks.length === 0) { + weeks.push({ label: dayLabel(key), chats: 0, tokens: 0, sessions: 0, errors: 0 }); + curKey = key; + } + const cur = weeks[weeks.length - 1]; + cur.chats += r.chats; + cur.tokens += r.total_tokens; + cur.sessions += r.sessions; + cur.errors += r.errors; + } + return weeks; +} + +/** Percent change current vs previous (null when previous is 0/empty). */ +export function growthRate(current: number, previous: number): number | null { + if (!Number.isFinite(current) || !Number.isFinite(previous) || previous <= 0) return null; + return (current - previous) / previous; +} + +export function formatGrowth(ratio: number | null): { text: string; up: boolean | null } { + if (ratio === null) return { text: "—", up: null }; + const pct = `${ratio >= 0 ? "+" : ""}${(ratio * 100).toFixed(1)}%`; + return { text: pct, up: ratio >= 0 }; +} + +/** Consecutive active (chats > 0) days ending today/yesterday. */ +export function currentStreak(rows: T[], todayIso?: string): number { + const today = (todayIso ?? new Date().toISOString().slice(0, 10)).slice(0, 10); + const byDay = new Map(rows.map((r) => [r.day.slice(0, 10), r.chats])); + let streak = 0; + const cursor = new Date(`${today}T00:00:00Z`); + if ((byDay.get(today) ?? 0) === 0) cursor.setUTCDate(cursor.getUTCDate() - 1); + for (;;) { + const key = cursor.toISOString().slice(0, 10); + if ((byDay.get(key) ?? 0) > 0) { + streak++; + cursor.setUTCDate(cursor.getUTCDate() - 1); + } else break; + } + return streak; +} + +const DOW_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + +export function dowName(dow: number): string { + return DOW_NAMES[((dow % 7) + 7) % 7]; +} + +export function hourLabel(hour: number): string { + const h = ((hour % 24) + 24) % 24; + if (h === 0) return "12a"; + if (h === 12) return "12p"; + return h < 12 ? `${h}a` : `${h - 12}p`; +} + +/** Weekday (Mon–Fri) vs weekend chats/tokens from heat cells. */ +export function weekdaySplit(cells: { dow: number; chats: number; tokens: number }[]): { + weekday: { chats: number; tokens: number }; + weekend: { chats: number; tokens: number }; +} { + const weekday = { chats: 0, tokens: 0 }; + const weekend = { chats: 0, tokens: 0 }; + for (const c of cells) { + const target = c.dow === 0 || c.dow === 6 ? weekend : weekday; + target.chats += c.chats; + target.tokens += c.tokens; + } + return { weekday, weekend }; +} + +/** Peak (top-6 active hours) vs off-hours chats from heat cells. */ +export function peakSplit(cells: { hour: number; chats: number }[]): { + peak: { hours: number[]; chats: number }; + off: { chats: number }; +} { + const byHour = new Map(); + let total = 0; + for (const c of cells) { + byHour.set(c.hour, (byHour.get(c.hour) ?? 0) + c.chats); + total += c.chats; + } + const ranked = [...byHour.entries()].sort((a, b) => b[1] - a[1]); + const peakHours = ranked.slice(0, 6).map(([h]) => h).sort((a, b) => a - b); + const peakChats = ranked.slice(0, 6).reduce((n, [, c]) => n + c, 0); + return { peak: { hours: peakHours, chats: peakChats }, off: { chats: total - peakChats } }; +} + +export const TOKEN_BUCKETS = ["0-100", "100-500", "500-1k", "1k-2k", "2k-4k", "4k-8k", "8k+"]; +export const DURATION_BUCKETS = ["<1m", "1-5m", "5-15m", "15-60m", ">60m"]; + +/** Fill missing buckets with zeros in canonical order. */ +export function orderBuckets(counts: { bucket: string; count: number }[], order: string[]): { bucket: string; count: number }[] { + const byBucket = new Map(counts.map((c) => [c.bucket, c.count])); + return order.map((bucket) => ({ bucket, count: byBucket.get(bucket) ?? 0 })); +} diff --git a/website/lib/dashboard-shapes.test.ts b/website/lib/dashboard-shapes.test.ts new file mode 100644 index 00000000..5894efab --- /dev/null +++ b/website/lib/dashboard-shapes.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import { formatCompact } from "./dashboard-format"; +import type { + ActivityEvent, + DayRow, + ModelRow, +} from "./dashboard-client"; + +/** + * Chart-shape guards: the UI only renders personal rows (verified by backend + * tests), and these assertions pin the shapes the charts consume so a backend + * drift fails here instead of silently rendering global-looking data. + */ +function assertDayRow(r: DayRow) { + expect(typeof r.day).toBe("string"); + for (const k of ["chats", "events", "total_tokens", "local_tokens", "cloud_tokens", "local_chats", "cloud_chats", "successes", "sessions", "errors"] as const) { + expect(typeof r[k]).toBe("number"); + } + void formatCompact(r.total_tokens); +} + +describe("dashboard payload shapes", () => { + it("accepts a realistic per-day series", () => { + const rows: DayRow[] = [ + { day: "2026-09-20", chats: 0, events: 0, input_tokens: 0, output_tokens: 0, total_tokens: 0, local_tokens: 0, cloud_tokens: 0, local_chats: 0, cloud_chats: 0, successes: 0, sessions: 0, errors: 0, avg_latency_ms: null, avg_ttft_ms: null }, + { day: "2026-09-21", chats: 3, events: 3, input_tokens: 250, output_tokens: 550, total_tokens: 800, local_tokens: 600, cloud_tokens: 200, local_chats: 2, cloud_chats: 1, successes: 2, sessions: 1, errors: 1, avg_latency_ms: 1667, avg_ttft_ms: 400 }, + ]; + rows.forEach(assertDayRow); + expect(rows.reduce((n, r) => n + r.chats, 0)).toBe(3); + }); + + it("accepts model rows and activity events", () => { + const models: ModelRow[] = [ + { model_name: "qwen3-0.6b", provider_name: null, engine_type: "local", chats: 2, events: 2, total_tokens: 600, avg_latency_ms: 1000, failed: 0 }, + ]; + expect(models[0].total_tokens).toBe(600); + const events: ActivityEvent[] = [ + { event_id: "e1", event_type: "chat_completed", model_name: "qwen3-0.6b", provider_name: null, engine_type: "local", total_tokens: 300, latency_ms: 1000, success: true, error_code: null, device_name: "Pixel", created_at: "2026-09-21T13:00:00.000Z" }, + ]; + expect(events[0].success).toBe(true); + }); +}); diff --git a/website/lib/firebase-client.ts b/website/lib/firebase-client.ts new file mode 100644 index 00000000..2946d187 --- /dev/null +++ b/website/lib/firebase-client.ts @@ -0,0 +1,96 @@ +/** + * Phase 1 — Firebase web client (shared Firebase project with the Android app). + * + * Uses only NEXT_PUBLIC_* env vars. Firebase web config (apiKey, projectId, …) + * is public by design — it is not a secret. The ID token is the credential; + * it is sent as `Authorization: Bearer ` and verified server-side + * with the Admin SDK. Never send a user id; the backend derives identity + * from the verified token (`decoded.uid`). + */ +import { getApp, getApps, initializeApp, type FirebaseApp } from "firebase/app"; +import { + getAuth, + GoogleAuthProvider, + GithubAuthProvider, + signInWithPopup, + signOut as firebaseSignOut, + onAuthStateChanged, + type Auth, + type User, +} from "firebase/auth"; + +const firebaseConfig = { + apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, + authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, + messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, +}; + +let app: FirebaseApp | null = null; +let auth: Auth | null = null; + +export function isFirebaseConfigured(): boolean { + return Boolean( + firebaseConfig.apiKey && firebaseConfig.authDomain && firebaseConfig.projectId && firebaseConfig.appId, + ); +} + +function getFirebaseApp(): FirebaseApp | null { + if (!isFirebaseConfigured()) return null; + if (app) return app; + app = getApps().length > 0 ? getApp() : initializeApp(firebaseConfig); + return app; +} + +export function getFirebaseAuth(): Auth | null { + if (auth) return auth; + const firebaseApp = getFirebaseApp(); + if (!firebaseApp) return null; + auth = getAuth(firebaseApp); + return auth; +} + +/** Google sign-in (same Firebase project as the Android app). */ +export async function signInWithGoogle(): Promise { + const firebaseAuth = getFirebaseAuth(); + if (!firebaseAuth) throw new Error("Firebase is not configured (missing NEXT_PUBLIC_FIREBASE_* env)."); + const result = await signInWithPopup(firebaseAuth, new GoogleAuthProvider()); + return result.user; +} + +/** GitHub sign-in (same Firebase project as the Android app). */ +export async function signInWithGitHub(): Promise { + const firebaseAuth = getFirebaseAuth(); + if (!firebaseAuth) throw new Error("Firebase is not configured (missing NEXT_PUBLIC_FIREBASE_* env)."); + const provider = new GithubAuthProvider(); + provider.addScope("read:user"); + provider.addScope("user:email"); + const result = await signInWithPopup(firebaseAuth, provider); + return result.user; +} + +export async function signOut(): Promise { + const firebaseAuth = getFirebaseAuth(); + if (firebaseAuth) await firebaseSignOut(firebaseAuth); +} + +/** Current user's Firebase ID token, or null when signed out / unconfigured. */ +export async function getIdToken(forceRefresh = false): Promise { + const firebaseAuth = getFirebaseAuth(); + const user = firebaseAuth?.currentUser; + if (!user) return null; + return user.getIdToken(forceRefresh); +} + +export function subscribeToAuthChanges(callback: (user: User | null) => void): () => void { + const firebaseAuth = getFirebaseAuth(); + if (!firebaseAuth) { + callback(null); + return () => {}; + } + return onAuthStateChanged(firebaseAuth, callback); +} + +export type { User }; diff --git a/website/lib/site.ts b/website/lib/site.ts index 5b0f053f..2a5692f3 100644 --- a/website/lib/site.ts +++ b/website/lib/site.ts @@ -59,4 +59,5 @@ export const navigation = [ { label: "Changelog", href: "/changelog" }, { label: "Docs", href: "/docs" }, { label: "Community", href: "/community" }, + { label: "Dashboard", href: "/dashboard" }, ]; \ No newline at end of file diff --git a/website/package-lock.json b/website/package-lock.json index b7bb8441..416beefb 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -16,8 +16,10 @@ "@radix-ui/react-slot": "^1.3.3", "@radix-ui/react-tabs": "^1.1.13", "@tailwindcss/typography": "^0.5.20", + "canvas-confetti": "^1.9.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "firebase": "^11.10.0", "framer-motion": "^12.23.24", "gsap": "^3.15.0", "lucide-react": "^0.525.0", @@ -28,10 +30,13 @@ "react-dom": "^19.1.0", "react-icons": "^5.5.0", "react-syntax-highlighter": "^15.6.1", + "recharts": "^3.10.1", "remark-gfm": "^4.0.1", "tailwind-merge": "^3.3.1" }, "devDependencies": { + "@testing-library/react": "^16.3.3", + "@types/canvas-confetti": "^1.9.0", "@types/node": "^22.15.21", "@types/react": "^19.1.6", "@types/react-dom": "^19.1.6", @@ -39,9 +44,11 @@ "autoprefixer": "^10.4.21", "eslint": "^8.57.1", "eslint-config-next": "15.5.7", + "jsdom": "^30.1.0", "postcss": "^8.5.3", "tailwindcss": "^3.4.17", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "^3.2.7" } }, "node_modules/@alloc/quick-lru": { @@ -56,6 +63,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.0.1.tgz", + "integrity": "sha512-C9duntabagkBZ1LebM7FKmphR4Q1pBclLxVbZETQV0akkFjV0ooFxo8FlvAQyKj9F6l8rEnmidgcTlpyxFYizg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.4.0", + "@csstools/css-color-parser": "^4.2.3", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.1", + "lru-cache": "^11.5.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-9.2.1.tgz", + "integrity": "sha512-NT4s3yZLjovPpliRpTvdsdzyPjqRqiCZj9MxnarBihbaY5MbAG7DWAJcrLlhmC7xKTNCXsTELcqB8YsqpLnUFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.1.0", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -88,1470 +128,3214 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" } }, - "node_modules/@emnapi/runtime": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", - "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "node_modules/@csstools/css-calc": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.0.tgz", + "integrity": "sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", - "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "node_modules/@csstools/css-color-parser": { + "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": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.4.3" + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.4.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=20.19.0" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.14.tgz", + "integrity": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==", "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } } }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.1.tgz", + "integrity": "sha512-bPlN9S9O1A0euCpEWE4qnvB5YDuyYVsUTrxSgmAM1Is0j4tICHoVyOVAXfWMP/kS9ZrjvyIXWV2PmomiAXXqOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@fontsource/fraunces": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@fontsource/fraunces/-/fraunces-5.3.0.tgz", - "integrity": "sha512-LEHTOBiZufQzAvnAe2rZ8ZknbDS28Q9kzwb0L+gZJEmjLGr8hhZ/5NuHXwDuHBWhVXYoiGGXmrb0H6Ko/RrHTg==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, - "node_modules/@fontsource/jetbrains-mono": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz", - "integrity": "sha512-fqDfB5I9f1p1TV486aUgB9t8zP84P0O1FtQR5Ol9vjwPy+S+EIGlVYm1cvj2W5shcZMTg2nZFdVMoH5wFu8a1A==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" + "node": ">=20.19.0" } }, - "node_modules/@fontsource/public-sans": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@fontsource/public-sans/-/public-sans-5.3.0.tgz", - "integrity": "sha512-kjODI0S3zdv0mBYCIQ8TbBayaiqszpc2UbhJiO3bjIqVVXzcWfHSt2o3WBCLOY3juaGaQoy4MoWCcgmfI5hCuA==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" } }, - "node_modules/@gsap/react": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@gsap/react/-/react-2.1.2.tgz", - "integrity": "sha512-JqliybO1837UcgH2hVOM4VO+38APk3ECNrsuSM4MuXp+rbf+/2IG2K1YJiqfTcXQHH7XlA0m3ykniFYstfq0Iw==", - "license": "SEE LICENSE AT https://gsap.com/standard-license", - "peerDependencies": { - "gsap": "^3.12.5", - "react": ">=17" + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" + "tslib": "^2.4.0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@esbuild/aix-ppc64": { + "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" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "optional": true, + "os": [ + "android" + ], "engines": { "node": ">=18" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", - "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "node_modules/@esbuild/android-arm64": { + "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" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "android" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.3" + "node": ">=18" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", - "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.3" + "node": ">=18" } }, - "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", - "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", - "license": "Apache-2.0", + "node_modules/@esbuild/darwin-x64": { + "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" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ - "freebsd" + "darwin" ], - "dependencies": { - "@img/sharp-wasm32": "0.35.4" - }, "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", - "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "node_modules/@esbuild/freebsd-arm64": { + "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" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "freebsd" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", - "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "node_modules/@esbuild/freebsd-x64": { + "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" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "freebsd" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", - "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "node_modules/@esbuild/linux-arm": { + "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" ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", - "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", - "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "node_modules/@esbuild/linux-ia32": { + "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": [ - "ppc64" - ], - "libc": [ - "glibc" + "ia32" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", - "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "node_modules/@esbuild/linux-loong64": { + "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": [ - "riscv64" - ], - "libc": [ - "glibc" + "loong64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", - "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ - "s390x" + "mips64el" ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", - "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ - "x64" - ], - "libc": [ - "glibc" + "ppc64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", - "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "node_modules/@esbuild/linux-riscv64": { + "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": [ - "arm64" - ], - "libc": [ - "musl" + "riscv64" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", - "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ - "x64" - ], - "libc": [ - "musl" + "s390x" ], - "license": "LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", - "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "node_modules/@esbuild/linux-x64": { + "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": [ - "arm" - ], - "libc": [ - "glibc" + "x64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.3" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", - "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "node_modules/@esbuild/netbsd-arm64": { + "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" ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "netbsd" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.3" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", - "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ - "ppc64" - ], - "libc": [ - "glibc" + "x64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "netbsd" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.3" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", - "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.3" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", - "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "node_modules/@esbuild/openbsd-arm64": { + "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": [ - "s390x" - ], - "libc": [ - "glibc" + "arm64" ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "openbsd" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.3" + "node": ">=18" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", - "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "openbsd" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.3" + "node": ">=18" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", - "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], - "libc": [ - "musl" - ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "openharmony" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + "node": ">=18" } }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", - "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], - "libc": [ - "musl" - ], - "license": "Apache-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.3" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", - "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.11.3" - }, - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", - "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", - "cpu": [ - "wasm32" + "sunos" ], - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@img/sharp-wasm32": "0.35.4" - }, "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", - "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "node_modules/@esbuild/win32-arm64": { + "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" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", - "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "node_modules/@esbuild/win32-ia32": { + "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" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": "^20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", - "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "node_modules/@esbuild/win32-x64": { + "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" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=18" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "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==", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": "^12.0.0 || ^14.0.0 || >=16.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==", - "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==", + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@mdx-js/mdx": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", - "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdx": "^2.0.0", - "acorn": "^8.0.0", - "collapse-white-space": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-util-scope": "^1.0.0", - "estree-walker": "^3.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "markdown-extensions": "^2.0.0", - "recma-build-jsx": "^1.0.0", - "recma-jsx": "^1.0.0", - "recma-stringify": "^1.0.0", - "rehype-recma": "^1.0.0", - "remark-mdx": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "source-map": "^0.7.0", - "unified": "^11.0.0", - "unist-util-position-from-estree": "^2.0.0", - "unist-util-stringify-position": "^4.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@mdx-js/react": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", - "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "node_modules/@exodus/bytes": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.2.tgz", + "integrity": "sha512-ctkytT+9TXtv6uiVj6Y7IjKh1+PdBGAD34xd0PpQ/qF+R7O/hoEQbZmN89OaCEaZn5I1A9+ZoPxD03unuFhyhQ==", + "dev": true, "license": "MIT", - "dependencies": { - "@types/mdx": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@types/react": ">=16", - "react": ">=16" + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", - "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", - "dev": true, - "license": "MIT", - "optional": true, + "node_modules/@firebase/ai": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-1.4.1.tgz", + "integrity": "sha512-bcusQfA/tHjUjBTnMx6jdoPMpDl3r8K15Z+snHz9wq0Foox0F/V+kNLXucEOHoTL2hTc9l+onZCyBJs2QoIC3g==", + "license": "Apache-2.0", "dependencies": { - "@tybys/wasm-util": "^0.10.3" + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=23.5.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" + "node": ">=18.0.0" }, "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" } }, - "node_modules/@next/env": { - "version": "15.5.24", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.24.tgz", - "integrity": "sha512-mBDF7T0XKZjs9SpUAl0buizVO+O02ULjOvWX8o/AZo/5AGw/UAS1Zzcylmd4pqbftzmKQi+L/nB4jgBYKEAl5Q==", - "license": "MIT" - }, - "node_modules/@next/eslint-plugin-next": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.7.tgz", - "integrity": "sha512-DtRU2N7BkGr8r+pExfuWHwMEPX5SD57FeA6pxdgCHODo+b/UgIgjE+rgWKtJAbEbGhVZ2jtHn4g3wNhWFoNBQQ==", - "dev": true, - "license": "MIT", + "node_modules/@firebase/analytics": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.17.tgz", + "integrity": "sha512-n5vfBbvzduMou/2cqsnKrIes4auaBjdhg8QNA2ZQZ59QgtO2QiwBaXQZQE4O4sgB0Ds1tvLgUUkY+pwzu6/xEg==", + "license": "Apache-2.0", "dependencies": { - "fast-glob": "3.3.1" + "@firebase/component": "0.6.18", + "@firebase/installations": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" } }, - "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.24", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.24.tgz", - "integrity": "sha512-AGdNLvxZNY6eR2iSnV+6wUa8CiHTMr4F7g3uHH7fT4ICIJBE00R9u4tzN/Vuwsw0cOi8MTD2HJcTCb6siMH88Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" + "node_modules/@firebase/analytics-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.23.tgz", + "integrity": "sha512-3AdO10RN18G5AzREPoFgYhW6vWXr3u+OYQv6pl3CX6Fky8QRk0AHurZlY3Q1xkXO0TDxIsdhO3y65HF7PBOJDw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.17", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.6.18", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" } }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.5.24", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.24.tgz", - "integrity": "sha512-9HrQajBMmGcrrrvDfRimiCrbAPh3E6uHJmwBovYr6Yrmi9p9PZqI876BrXX280wICh3o2XwUlp4blkB0NNBqFg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@firebase/analytics-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", + "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.13.2.tgz", + "integrity": "sha512-jwtMmJa1BXXDCiDx1vC6SFN/+HfYG53UkfJa6qeN5ogvOunzbFDO3wISZy5n9xgYFUrEP6M7e8EG++riHNTv9w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, "engines": { - "node": ">= 10" + "node": ">=18.0.0" } }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.24", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.24.tgz", - "integrity": "sha512-rl9LSfE75si0WT3cDgdUC1XYCKS+TgxC+/IjitmeycrAG18X/plIP1/vy8dd/HPycYcIvE688PD7FuvEAiEAew==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@firebase/app-check": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.10.1.tgz", + "integrity": "sha512-MgNdlms9Qb0oSny87pwpjKush9qUwCJhfmTJHDfrcKo4neLGiSeVE4qJkzP7EQTIUFKp84pbTxobSAXkiuQVYQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, "engines": { - "node": ">= 10" + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" } }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.24", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.24.tgz", - "integrity": "sha512-TlNAnpsjxSF3aAUtqnfmtXXf8m9sIDBlmF3c7bTAlnshUYu2U0OxN2uf5d0gcFwqHVEdivJNBcCaqNOwPGNimw==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@firebase/app-check-compat": { + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.26.tgz", + "integrity": "sha512-PkX+XJMLDea6nmnopzFKlr+s2LMQGqdyT2DHdbx1v1dPSqOol2YzgpgymmhC67vitXVpNvS3m/AiWQWWhhRRPQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.10.1", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, "engines": { - "node": ">= 10" + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" } }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.24", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.24.tgz", - "integrity": "sha512-7dwtlhr0SLndqTG1z9ncRkbJswDZiKWlxzFyXDvJ2RDZRDRHp8zyMJ4D9UH/FgnQeXxxB6gZy2pMcIUoNKQ4pA==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", + "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.4.2.tgz", + "integrity": "sha512-LssbyKHlwLeiV8GBATyOyjmHcMpX/tFjzRUCS1jnwGAew1VsBB4fJowyS5Ud5LdFbYpJeS+IQoC+RQxpK7eH3Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.13.2", + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, "engines": { - "node": ">= 10" + "node": ">=18.0.0" } }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.24", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.24.tgz", - "integrity": "sha512-kGZxM+WhkYs0276lFrMkj7PRtXT3Btp6cwvfSO/cCVLxJJttB5Ccnl2niaCgUja8HgSbEVnMHpg3FJWoOJ9e/g==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth": { + "version": "1.10.8", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.8.tgz", + "integrity": "sha512-GpuTz5ap8zumr/ocnPY57ZanX02COsXloY6Y/2LYPAuXYiaJRf6BAGDEdRq1BMjP93kqQnKNuKZUTMZbQ8MNYA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, "engines": { - "node": ">= 10" + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } } }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.24", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.24.tgz", - "integrity": "sha512-jBDDkZ/qKAqkWivWDMkJSXUzbzV0QKRBKJjEHUAvSB97Hzw7NLzJ6yV56Lts/wjir7s4P31GYgpbS6ZL+hasAA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "node_modules/@firebase/auth-compat": { + "version": "0.5.28", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.28.tgz", + "integrity": "sha512-HpMSo/cc6Y8IX7bkRIaPPqT//Jt83iWy5rmDWeThXQCAImstkdNo3giFLORJwrZw2ptiGkOij64EH1ztNJzc7Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.10.8", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.6.18", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, "engines": { - "node": ">= 10" + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" } }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.24", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.24.tgz", - "integrity": "sha512-JqtwjvvorjacQ0spgjmUJoxySoYgPwdT1sFdQ0/zmW4iMlP2hjYlCoJIyS7o6Epb4Fug8eco3HXFoBDUCDeH7Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", + "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", + "node_modules/@firebase/component": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.18.tgz", + "integrity": "sha512-n28kPCkE2dL2U28fSxZJjzPPVpKsQminJ6NrzcKXAI0E/lYC8YhfwpyllScqVEvAI3J2QgJZWYgrX+1qGI+SQQ==", + "license": "Apache-2.0", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" }, "engines": { - "node": ">= 8" + "node": ">=18.0.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" + "node_modules/@firebase/data-connect": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.10.tgz", + "integrity": "sha512-VMVk7zxIkgwlVQIWHOKFahmleIjiVFwFOjmakXPd/LDgaB/5vzwsB5DWIYo+3KhGxWpidQlR8geCIn39YflJIQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", + "node_modules/@firebase/database": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.20.tgz", + "integrity": "sha512-H9Rpj1pQ1yc9+4HQOotFGLxqAXwOzCHsRSRjcQFNOr8lhUt6LeYjf0NSRL04sc4X0dWe8DsCvYKxMYvFG/iOJw==", + "license": "Apache-2.0", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" }, "engines": { - "node": ">= 8" + "node": ">=18.0.0" } }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", - "dev": true, - "license": "MIT", + "node_modules/@firebase/database-compat": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.11.tgz", + "integrity": "sha512-itEsHARSsYS95+udF/TtIzNeQ0Uhx4uIna0sk4E0wQJBUnLc/G1X6D7oRljoOuwwCezRLGvWBRyNrugv/esOEw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/database": "1.0.20", + "@firebase/database-types": "1.0.15", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, "engines": { - "node": ">=12.4.0" + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", - "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", - "license": "MIT" + "node_modules/@firebase/database-types": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.15.tgz", + "integrity": "sha512-XWHJ0VUJ0k2E9HDMlKxlgy/ZuTa9EvHCGLjaKSUvrQnwhgZuRU5N3yX6SZ+ftf2hTzZmfRkv+b3QRvGg40bKNw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.12.1" + } }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", - "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", - "license": "MIT", + "node_modules/@firebase/firestore": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.8.0.tgz", + "integrity": "sha512-QSRk+Q1/CaabKyqn3C32KSFiOdZpSqI9rpLK5BHPcooElumOBooPFa6YkDdiT+/KhJtel36LdAacha9BptMj2A==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-slot": "1.3.3" + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "@firebase/webchannel-wrapper": "1.0.3", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">=18.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@firebase/app": "0.x" } }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", - "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "node_modules/@firebase/firestore-compat": { + "version": "0.3.53", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.53.tgz", + "integrity": "sha512-qI3yZL8ljwAYWrTousWYbemay2YZa+udLWugjdjju2KODWtLG94DfO4NALJgPLv8CVGcDHNFXoyQexdRA0Cz8Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/firestore": "4.8.0", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" } }, - "node_modules/@radix-ui/react-context": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", - "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", - "license": "MIT", + "node_modules/@firebase/firestore-types": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", + "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", + "license": "Apache-2.0", "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" } }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.23", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", - "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", - "license": "MIT", + "node_modules/@firebase/functions": { + "version": "0.12.9", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.12.9.tgz", + "integrity": "sha512-FG95w6vjbUXN84Ehezc2SDjGmGq225UYbHrb/ptkRT7OTuCiQRErOQuyt1jI1tvcDekdNog+anIObihNFz79Lg==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-dismissable-layer": "1.1.19", - "@radix-ui/react-focus-guards": "1.1.6", - "@radix-ui/react-focus-scope": "1.1.16", - "@radix-ui/react-id": "1.1.4", - "@radix-ui/react-portal": "1.1.17", - "@radix-ui/react-presence": "1.1.10", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-slot": "1.3.3", - "@radix-ui/react-use-controllable-state": "1.2.6", - "@radix-ui/react-use-layout-effect": "1.1.4", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.7.2" + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.18", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">=18.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@firebase/app": "0.x" } }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", - "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "node_modules/@firebase/functions-compat": { + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.26.tgz", + "integrity": "sha512-A798/6ff5LcG2LTWqaGazbFYnjBW8zc65YfID/en83ALmkhu2b0G8ykvQnLtakbV9ajrMYPn7Yc/XcYsZIUsjA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/functions": "0.12.9", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" } }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", - "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", - "license": "MIT", + "node_modules/@firebase/functions-types": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", + "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.18.tgz", + "integrity": "sha512-NQ86uGAcvO8nBRwVltRL9QQ4Reidc/3whdAasgeWCPIcrhOKDuNpAALa6eCVryLnK14ua2DqekCOX5uC9XbU/A==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-use-callback-ref": "1.1.4", - "@radix-ui/react-use-effect-event": "0.0.5" + "@firebase/component": "0.6.18", + "@firebase/util": "1.12.1", + "idb": "7.1.1", + "tslib": "^2.1.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.18", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.18.tgz", + "integrity": "sha512-aLFohRpJO5kKBL/XYL4tN+GdwEB/Q6Vo9eZOM/6Kic7asSUgmSfGPpGUZO1OAaSRGwF4Lqnvi1f/f9VZnKzChw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/installations": "0.6.18", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "peerDependencies": { + "@firebase/app-compat": "0.x" } }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", - "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", - "license": "MIT", + "node_modules/@firebase/installations-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", + "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", + "license": "Apache-2.0", "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.4.tgz", + "integrity": "sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", - "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", - "license": "MIT", + "node_modules/@firebase/messaging": { + "version": "0.12.22", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.22.tgz", + "integrity": "sha512-GJcrPLc+Hu7nk+XQ70Okt3M1u1eRr2ZvpMbzbc54oTPJZySHcX9ccZGVFcsZbSZ6o1uqumm8Oc7OFkD3Rn1/og==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-use-callback-ref": "1.1.4" + "@firebase/component": "0.6.18", + "@firebase/installations": "0.6.18", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.12.1", + "idb": "7.1.1", + "tslib": "^2.1.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "@firebase/app": "0.x" } }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", - "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", - "license": "MIT", + "node_modules/@firebase/messaging-compat": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.22.tgz", + "integrity": "sha512-5ZHtRnj6YO6f/QPa/KU6gryjmX4Kg33Kn4gRpNU6M1K47Gm8kcQwPkX7erRUYEH1mIWptfvjvXMHWoZaWjkU7A==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.4" + "@firebase/component": "0.6.18", + "@firebase/messaging": "0.12.22", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" }, "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "@firebase/app-compat": "0.x" } }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", - "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", - "license": "MIT", + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", + "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.7.tgz", + "integrity": "sha512-JTlTQNZKAd4+Q5sodpw6CN+6NmwbY72av3Lb6wUKTsL7rb3cuBIhQSrslWbVz0SwK3x0ZNcqX24qtRbwKiv+6w==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-use-layout-effect": "1.1.4" + "@firebase/component": "0.6.18", + "@firebase/installations": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "@firebase/app": "0.x" } }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", - "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", - "license": "MIT", + "node_modules/@firebase/performance-compat": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.20.tgz", + "integrity": "sha512-XkFK5NmOKCBuqOKWeRgBUFZZGz9SzdTZp4OqeUg+5nyjapTiZ4XoiiUL8z7mB2q+63rPmBl7msv682J3rcDXIQ==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.4" + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/performance": "0.7.7", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "@firebase/app-compat": "0.x" } }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", - "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", - "license": "MIT", + "node_modules/@firebase/performance-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", + "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.6.5.tgz", + "integrity": "sha512-fU0c8HY0vrVHwC+zQ/fpXSqHyDMuuuglV94VF6Yonhz8Fg2J+KOowPGANM0SZkLvVOYpTeWp3ZmM+F6NjwWLnw==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-slot": "1.3.3" + "@firebase/component": "0.6.18", + "@firebase/installations": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "@firebase/app": "0.x" } }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", - "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", - "license": "MIT", + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.18", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.18.tgz", + "integrity": "sha512-YiETpldhDy7zUrnS8e+3l7cNs0sL7+tVAxvVYU0lu7O+qLHbmdtAxmgY+wJqWdW2c9nDvBFec7QiF58pEUu0qQ==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-collection": "1.1.15", - "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-direction": "1.1.4", - "@radix-ui/react-id": "1.1.4", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-use-callback-ref": "1.1.4", - "@radix-ui/react-use-controllable-state": "1.2.6", - "@radix-ui/react-use-is-hydrated": "0.1.3", - "@radix-ui/react-use-layout-effect": "1.1.4" + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/remote-config": "0.6.5", + "@firebase/remote-config-types": "0.4.0", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.4.0.tgz", + "integrity": "sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.13.14.tgz", + "integrity": "sha512-xTq5ixxORzx+bfqCpsh+o3fxOsGoDjC1nO0Mq2+KsOcny3l7beyBhP/y1u5T6mgsFQwI1j6oAkbT5cWdDBx87g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", - "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", - "license": "MIT", + "node_modules/@firebase/storage-compat": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.24.tgz", + "integrity": "sha512-XHn2tLniiP7BFKJaPZ0P8YQXKiVJX+bMyE2j2YWjYfaddqiJnROJYqSomwW6L3Y+gZAga35ONXUJQju6MB6SOQ==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.5" + "@firebase/component": "0.6.18", + "@firebase/storage": "0.13.14", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" }, "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", + "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.12.1.tgz", + "integrity": "sha512-zGlBn/9Dnya5ta9bX/fgEoNC3Cp8s6h+uYPYaDieZsFOAdHP/ExzQ/eaDgxD3GOROdPkLKpvKY0iIzr9adle0w==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", - "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", - "license": "MIT", + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.3.tgz", + "integrity": "sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==", + "license": "Apache-2.0" + }, + "node_modules/@fontsource/fraunces": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/fraunces/-/fraunces-5.3.0.tgz", + "integrity": "sha512-LEHTOBiZufQzAvnAe2rZ8ZknbDS28Q9kzwb0L+gZJEmjLGr8hhZ/5NuHXwDuHBWhVXYoiGGXmrb0H6Ko/RrHTg==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/jetbrains-mono": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz", + "integrity": "sha512-fqDfB5I9f1p1TV486aUgB9t8zP84P0O1FtQR5Ol9vjwPy+S+EIGlVYm1cvj2W5shcZMTg2nZFdVMoH5wFu8a1A==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/public-sans": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/public-sans/-/public-sans-5.3.0.tgz", + "integrity": "sha512-kjODI0S3zdv0mBYCIQ8TbBayaiqszpc2UbhJiO3bjIqVVXzcWfHSt2o3WBCLOY3juaGaQoy4MoWCcgmfI5hCuA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.16.tgz", + "integrity": "sha512-wE4Ut/olIzfKqp631XrG+wbF0v1vWFN4YL9FyXC2LJiG33DsV7PLzURjrCvY/6je2ntdRkeLpPDluzSRGaVltQ==", + "license": "Apache-2.0", "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-direction": "1.1.4", - "@radix-ui/react-id": "1.1.4", - "@radix-ui/react-presence": "1.1.10", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-roving-focus": "1.1.19", - "@radix-ui/react-use-controllable-state": "1.2.6" + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@gsap/react": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@gsap/react/-/react-2.1.2.tgz", + "integrity": "sha512-JqliybO1837UcgH2hVOM4VO+38APk3ECNrsuSM4MuXp+rbf+/2IG2K1YJiqfTcXQHH7XlA0m3ykniFYstfq0Iw==", + "license": "SEE LICENSE AT https://gsap.com/standard-license", "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "gsap": "^3.12.5", + "react": ">=17" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=10.10.0" } }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", - "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" } }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", - "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", + "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + } + }, + "node_modules/@next/env": { + "version": "15.5.24", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.24.tgz", + "integrity": "sha512-mBDF7T0XKZjs9SpUAl0buizVO+O02ULjOvWX8o/AZo/5AGw/UAS1Zzcylmd4pqbftzmKQi+L/nB4jgBYKEAl5Q==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.7.tgz", + "integrity": "sha512-DtRU2N7BkGr8r+pExfuWHwMEPX5SD57FeA6pxdgCHODo+b/UgIgjE+rgWKtJAbEbGhVZ2jtHn4g3wNhWFoNBQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.24", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.24.tgz", + "integrity": "sha512-AGdNLvxZNY6eR2iSnV+6wUa8CiHTMr4F7g3uHH7fT4ICIJBE00R9u4tzN/Vuwsw0cOi8MTD2HJcTCb6siMH88Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.24", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.24.tgz", + "integrity": "sha512-9HrQajBMmGcrrrvDfRimiCrbAPh3E6uHJmwBovYr6Yrmi9p9PZqI876BrXX280wICh3o2XwUlp4blkB0NNBqFg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.24.tgz", + "integrity": "sha512-rl9LSfE75si0WT3cDgdUC1XYCKS+TgxC+/IjitmeycrAG18X/plIP1/vy8dd/HPycYcIvE688PD7FuvEAiEAew==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.24.tgz", + "integrity": "sha512-TlNAnpsjxSF3aAUtqnfmtXXf8m9sIDBlmF3c7bTAlnshUYu2U0OxN2uf5d0gcFwqHVEdivJNBcCaqNOwPGNimw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.24.tgz", + "integrity": "sha512-7dwtlhr0SLndqTG1z9ncRkbJswDZiKWlxzFyXDvJ2RDZRDRHp8zyMJ4D9UH/FgnQeXxxB6gZy2pMcIUoNKQ4pA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.24", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.24.tgz", + "integrity": "sha512-kGZxM+WhkYs0276lFrMkj7PRtXT3Btp6cwvfSO/cCVLxJJttB5Ccnl2niaCgUja8HgSbEVnMHpg3FJWoOJ9e/g==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.24", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.24.tgz", + "integrity": "sha512-jBDDkZ/qKAqkWivWDMkJSXUzbzV0QKRBKJjEHUAvSB97Hzw7NLzJ6yV56Lts/wjir7s4P31GYgpbS6ZL+hasAA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.24", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.24.tgz", + "integrity": "sha512-JqtwjvvorjacQ0spgjmUJoxySoYgPwdT1sFdQ0/zmW4iMlP2hjYlCoJIyS7o6Epb4Fug8eco3HXFoBDUCDeH7Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", + "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.4.tgz", + "integrity": "sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.4.tgz", + "integrity": "sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.4.tgz", + "integrity": "sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.4.tgz", + "integrity": "sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.4.tgz", + "integrity": "sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.4.tgz", + "integrity": "sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.4.tgz", + "integrity": "sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.4.tgz", + "integrity": "sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.4.tgz", + "integrity": "sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.4.tgz", + "integrity": "sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.4.tgz", + "integrity": "sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.4.tgz", + "integrity": "sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.4.tgz", + "integrity": "sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.4.tgz", + "integrity": "sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.4.tgz", + "integrity": "sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.4.tgz", + "integrity": "sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.4.tgz", + "integrity": "sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.4.tgz", + "integrity": "sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.4.tgz", + "integrity": "sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.4.tgz", + "integrity": "sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.4.tgz", + "integrity": "sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.4.tgz", + "integrity": "sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-use-effect-event": "0.0.5", - "@radix-ui/react-use-layout-effect": "1.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", - "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.4.tgz", + "integrity": "sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.4" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", - "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.4.tgz", + "integrity": "sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", - "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.4.tgz", + "integrity": "sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@rtsao/scc": { "version": "1.1.0", @@ -1567,6 +3351,18 @@ "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==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1601,6 +3397,66 @@ "node": ">=4" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -1612,6 +3468,95 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/canvas-confetti": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz", + "integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -1621,6 +3566,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1677,7 +3629,6 @@ "version": "22.20.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1718,6 +3669,12 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.67.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", @@ -2341,24 +4298,139 @@ ], "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", - "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, "node_modules/acorn": { "version": "8.18.0", @@ -2402,7 +4474,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2412,7 +4483,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2638,6 +4708,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -2767,6 +4847,16 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2836,6 +4926,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -2925,6 +5025,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-confetti": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz", + "integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==", + "license": "ISC", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -2935,6 +5045,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2992,6 +5119,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -3046,6 +5183,20 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3069,7 +5220,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3082,7 +5232,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/comma-separated-tokens": { @@ -3126,6 +5275,20 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3144,6 +5307,127 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -3151,6 +5435,35 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "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/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -3222,6 +5535,19 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/decode-named-character-reference": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", @@ -3235,6 +5561,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3341,6 +5677,14 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3370,6 +5714,19 @@ "dev": true, "license": "MIT" }, + "node_modules/entities": { + "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": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -3505,6 +5862,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", @@ -3568,6 +5932,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz", + "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types", + "tests/browser-compat" + ] + }, "node_modules/esast-util-from-estree": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", @@ -3600,11 +5976,52 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/esbuild": { + "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", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@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/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4144,6 +6561,22 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -4223,6 +6656,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4265,6 +6710,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/firebase": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-11.10.0.tgz", + "integrity": "sha512-nKBXoDzF0DrXTBQJlZa+sbC5By99ysYU1D6PkMRYknm0nCW7rJly47q492Ht7Ndz5MeYSBuboKuhS1e6mFC03w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "1.4.1", + "@firebase/analytics": "0.10.17", + "@firebase/analytics-compat": "0.2.23", + "@firebase/app": "0.13.2", + "@firebase/app-check": "0.10.1", + "@firebase/app-check-compat": "0.3.26", + "@firebase/app-compat": "0.4.2", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.10.8", + "@firebase/auth-compat": "0.5.28", + "@firebase/data-connect": "0.3.10", + "@firebase/database": "1.0.20", + "@firebase/database-compat": "2.0.11", + "@firebase/firestore": "4.8.0", + "@firebase/firestore-compat": "0.3.53", + "@firebase/functions": "0.12.9", + "@firebase/functions-compat": "0.3.26", + "@firebase/installations": "0.6.18", + "@firebase/installations-compat": "0.2.18", + "@firebase/messaging": "0.12.22", + "@firebase/messaging-compat": "0.2.22", + "@firebase/performance": "0.7.7", + "@firebase/performance-compat": "0.2.20", + "@firebase/remote-config": "0.6.5", + "@firebase/remote-config-compat": "0.2.18", + "@firebase/storage": "0.13.14", + "@firebase/storage-compat": "0.3.24", + "@firebase/util": "1.12.1" + } + }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -4426,6 +6907,15 @@ "node": ">= 0.4" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4849,6 +7339,31 @@ "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", "license": "CC0-1.0" }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4859,6 +7374,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz", + "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4926,6 +7451,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -5157,6 +7691,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -5273,6 +7816,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -5488,6 +8038,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.0.tgz", + "integrity": "sha512-h/Q+H+KmlGf49ITw3TrykSVbg04S75S2O7ZtJc9gMGOWBrQRZmx5AJIKLtJ01e6Kszl+/tpFthCVqxNifxhoqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^7.0.0", + "@asamuzakjp/dom-selector": "^9.1.2", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.13", + "@exodus/bytes": "^1.15.1", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "tough-cookie": "^6.0.2", + "undici": "^8.10.2", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -5616,6 +8206,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5623,6 +8219,12 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -5646,6 +8248,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lowlight": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", @@ -5660,6 +8269,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/lucide-react": { "version": "0.525.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", @@ -5669,6 +8288,27 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/markdown-extensions": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", @@ -6000,6 +8640,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -7242,6 +9889,19 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7278,6 +9938,23 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7325,9 +10002,9 @@ } }, "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "funding": [ { "type": "opencollective", @@ -7344,7 +10021,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.17", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -7511,6 +10188,44 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -7542,6 +10257,29 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7606,9 +10344,31 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-remove-scroll": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", @@ -7716,6 +10476,36 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz", + "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/recma-build-jsx": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", @@ -7783,6 +10573,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -8038,6 +10843,31 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -8109,6 +10939,52 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "4.63.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.4.tgz", + "integrity": "sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.4", + "@rollup/rollup-android-arm64": "4.63.4", + "@rollup/rollup-darwin-arm64": "4.63.4", + "@rollup/rollup-darwin-x64": "4.63.4", + "@rollup/rollup-freebsd-arm64": "4.63.4", + "@rollup/rollup-freebsd-x64": "4.63.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.4", + "@rollup/rollup-linux-arm-musleabihf": "4.63.4", + "@rollup/rollup-linux-arm64-gnu": "4.63.4", + "@rollup/rollup-linux-arm64-musl": "4.63.4", + "@rollup/rollup-linux-loong64-gnu": "4.63.4", + "@rollup/rollup-linux-loong64-musl": "4.63.4", + "@rollup/rollup-linux-ppc64-gnu": "4.63.4", + "@rollup/rollup-linux-ppc64-musl": "4.63.4", + "@rollup/rollup-linux-riscv64-gnu": "4.63.4", + "@rollup/rollup-linux-riscv64-musl": "4.63.4", + "@rollup/rollup-linux-s390x-gnu": "4.63.4", + "@rollup/rollup-linux-x64-gnu": "4.63.4", + "@rollup/rollup-linux-x64-musl": "4.63.4", + "@rollup/rollup-openbsd-x64": "4.63.4", + "@rollup/rollup-openharmony-arm64": "4.63.4", + "@rollup/rollup-win32-arm64-msvc": "4.63.4", + "@rollup/rollup-win32-ia32-msvc": "4.63.4", + "@rollup/rollup-win32-x64-gnu": "4.63.4", + "@rollup/rollup-win32-x64-msvc": "4.63.4", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8152,6 +11028,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -8187,6 +11083,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -8404,6 +11313,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", @@ -8439,6 +11355,20 @@ "dev": true, "license": "MIT" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -8453,6 +11383,26 @@ "node": ">= 0.4" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -8585,7 +11535,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -8617,6 +11566,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -8829,6 +11798,26 @@ "node": ">=0.8" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -8867,13 +11856,63 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", - "engines": { - "node": ">=12" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "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.13" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "bin": { + "tldts": "bin/cli.js" } }, + "node_modules/tldts-core": { + "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" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8886,6 +11925,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -9081,11 +12146,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz", + "integrity": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unified": { @@ -9358,6 +12432,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9406,6 +12489,320 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.2.tgz", + "integrity": "sha512-TEZA+Zqxin7Jjsm2cjRohCmen5awh+hT6Zi3VZdqZlNRk7zvOI/9WpBFg/DWlA56bWnzwm6DuB8NS0EsxQH9uQ==", + "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/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9511,6 +12908,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -9521,6 +12935,23 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -9528,6 +12959,23 @@ "dev": true, "license": "ISC" }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -9537,6 +12985,15 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", @@ -9552,6 +13009,33 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/website/package.json b/website/package.json index 177b951d..a161f1b3 100644 --- a/website/package.json +++ b/website/package.json @@ -8,7 +8,8 @@ "build": "next build", "start": "next start", "lint": "next lint", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "vitest run" }, "dependencies": { "@fontsource/fraunces": "^5.2.5", @@ -19,8 +20,10 @@ "@radix-ui/react-slot": "^1.3.3", "@radix-ui/react-tabs": "^1.1.13", "@tailwindcss/typography": "^0.5.20", + "canvas-confetti": "^1.9.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "firebase": "^11.10.0", "framer-motion": "^12.23.24", "gsap": "^3.15.0", "lucide-react": "^0.525.0", @@ -31,10 +34,13 @@ "react-dom": "^19.1.0", "react-icons": "^5.5.0", "react-syntax-highlighter": "^15.6.1", + "recharts": "^3.10.1", "remark-gfm": "^4.0.1", "tailwind-merge": "^3.3.1" }, "devDependencies": { + "@testing-library/react": "^16.3.3", + "@types/canvas-confetti": "^1.9.0", "@types/node": "^22.15.21", "@types/react": "^19.1.6", "@types/react-dom": "^19.1.6", @@ -42,8 +48,10 @@ "autoprefixer": "^10.4.21", "eslint": "^8.57.1", "eslint-config-next": "15.5.7", + "jsdom": "^30.1.0", "postcss": "^8.5.3", "tailwindcss": "^3.4.17", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "^3.2.7" } } diff --git a/website/vitest.config.ts b/website/vitest.config.ts new file mode 100644 index 00000000..c2f1b14a --- /dev/null +++ b/website/vitest.config.ts @@ -0,0 +1,16 @@ +import path from "node:path"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + resolve: { + alias: { "@": path.resolve(__dirname, ".") }, + }, + esbuild: { + jsx: "automatic", + }, + test: { + environment: "jsdom", + include: ["lib/**/*.test.ts", "components/**/*.test.tsx", "app/**/*.test.tsx"], + setupFiles: ["./vitest.setup.ts"], + }, +}); diff --git a/website/vitest.setup.ts b/website/vitest.setup.ts new file mode 100644 index 00000000..b40c486c --- /dev/null +++ b/website/vitest.setup.ts @@ -0,0 +1,31 @@ +import { vi } from "vitest"; + +// Recharts measures with ResizeObserver (absent in jsdom) — fixed stub. +class StubResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +} +vi.stubGlobal("ResizeObserver", StubResizeObserver); + +// framer-motion's useInView needs IntersectionObserver (absent in jsdom). +class StubIntersectionObserver { + observe() {} + unobserve() {} + disconnect() {} + takeRecords() { + return []; + } +} +vi.stubGlobal("IntersectionObserver", StubIntersectionObserver); + +if (!window.matchMedia) { + window.matchMedia = (() => ({ + matches: false, + addListener() {}, + removeListener() {}, + addEventListener() {}, + removeEventListener() {}, + dispatchEvent: () => false, + })) as unknown as typeof window.matchMedia; +} From ff4ac2cfa617daabb048acd6dc6cec19f70019be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 16:36:14 +0000 Subject: [PATCH 2/2] chore(deps): bump @vitest/mocker and vitest in /website Bumps [@vitest/mocker](https://github.com/vitest-dev/vitest/tree/HEAD/packages/mocker) to 5.0.1 and updates ancestor dependency [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). These dependencies need to be updated together. Updates `@vitest/mocker` from 3.2.7 to 5.0.1 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.1/packages/mocker) Updates `vitest` from 3.2.7 to 5.0.1 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.1/packages/vitest) --- updated-dependencies: - dependency-name: "@vitest/mocker" dependency-version: 5.0.1 dependency-type: indirect - dependency-name: vitest dependency-version: 5.0.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- website/package-lock.json | 1730 +++++++++++++------------------------ website/package.json | 2 +- 2 files changed, 613 insertions(+), 1119 deletions(-) diff --git a/website/package-lock.json b/website/package-lock.json index 416beefb..d0d32311 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -48,7 +48,7 @@ "postcss": "^8.5.3", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", - "vitest": "^3.2.7" + "vitest": "^5.0.1" } }, "node_modules/@alloc/quick-lru": { @@ -314,448 +314,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", - "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", - "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", - "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", - "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", - "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", @@ -2189,26 +1747,6 @@ "react": ">=16" } }, - "node_modules/@napi-rs/lzma-linux-x64-gnu": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", - "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", @@ -2432,6 +1970,17 @@ "node": ">=12.4.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz", + "integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -2948,10 +2497,10 @@ } } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.4.tgz", - "integrity": "sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==", + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.9.tgz", + "integrity": "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==", "cpu": [ "arm" ], @@ -2960,12 +2509,16 @@ "optional": true, "os": [ "android" - ] + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.4.tgz", - "integrity": "sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.9.tgz", + "integrity": "sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==", "cpu": [ "arm64" ], @@ -2974,12 +2527,16 @@ "optional": true, "os": [ "android" - ] + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.4.tgz", - "integrity": "sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.9.tgz", + "integrity": "sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==", "cpu": [ "arm64" ], @@ -2988,12 +2545,16 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.4.tgz", - "integrity": "sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.9.tgz", + "integrity": "sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==", "cpu": [ "x64" ], @@ -3002,26 +2563,16 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.4.tgz", - "integrity": "sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.4.tgz", - "integrity": "sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.9.tgz", + "integrity": "sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==", "cpu": [ "x64" ], @@ -3030,84 +2581,38 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.4.tgz", - "integrity": "sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==", - "cpu": [ - "arm" ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.4.tgz", - "integrity": "sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.9.tgz", + "integrity": "sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==", "cpu": [ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.4.tgz", - "integrity": "sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.4.tgz", - "integrity": "sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.9.tgz", + "integrity": "sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.4.tgz", - "integrity": "sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==", - "cpu": [ - "loong64" - ], - "dev": true, "libc": [ "glibc" ], @@ -3115,14 +2620,18 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.4.tgz", - "integrity": "sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.9.tgz", + "integrity": "sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "libc": [ @@ -3132,50 +2641,20 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.4.tgz", - "integrity": "sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.4.tgz", - "integrity": "sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.9.tgz", + "integrity": "sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==", "cpu": [ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.4.tgz", - "integrity": "sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==", - "cpu": [ - "riscv64" - ], - "dev": true, "libc": [ "glibc" ], @@ -3183,29 +2662,16 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.4.tgz", - "integrity": "sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==", - "cpu": [ - "riscv64" ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.4.tgz", - "integrity": "sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.9.tgz", + "integrity": "sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==", "cpu": [ "s390x" ], @@ -3217,12 +2683,16 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.4.tgz", - "integrity": "sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.9.tgz", + "integrity": "sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==", "cpu": [ "x64" ], @@ -3234,12 +2704,16 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.4.tgz", - "integrity": "sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.9.tgz", + "integrity": "sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==", "cpu": [ "x64" ], @@ -3251,26 +2725,16 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.4.tgz", - "integrity": "sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.4.tgz", - "integrity": "sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.9.tgz", + "integrity": "sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==", "cpu": [ "arm64" ], @@ -3279,12 +2743,16 @@ "optional": true, "os": [ "openharmony" - ] + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.4.tgz", - "integrity": "sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.9.tgz", + "integrity": "sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==", "cpu": [ "arm64" ], @@ -3293,26 +2761,16 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.4.tgz", - "integrity": "sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==", - "cpu": [ - "ia32" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.4.tgz", - "integrity": "sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.9.tgz", + "integrity": "sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==", "cpu": [ "x64" ], @@ -3321,21 +2779,19 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.4.tgz", - "integrity": "sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==", - "cpu": [ - "x64" ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "peer": true }, "node_modules/@rtsao/scc": { "version": "1.1.0", @@ -4317,40 +3773,24 @@ "win32" ] }, - "node_modules/@vitest/expect": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", - "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/mocker": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", - "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "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": "3.2.7", + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.1", "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "magic-string": "^1.2.3" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -4361,73 +3801,12 @@ } } }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", - "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", - "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.7", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", - "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.7", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vitest/spy": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", - "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", - "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "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": "3.2.7", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, "funding": { "url": "https://opencollective.com/vitest" } @@ -4926,16 +4305,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -5046,18 +4415,11 @@ } }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -5119,16 +4481,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -5561,16 +4913,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5627,8 +4969,8 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -5863,9 +5205,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "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" }, @@ -5976,48 +5318,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/esbuild": { - "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", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@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/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -8108,68 +7408,353 @@ "dependencies": { "minimist": "^1.2.0" }, - "bin": { - "json5": "lib/cli.js" + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, "engines": { - "node": ">=4.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "CC0-1.0" + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, "engines": { - "node": ">=0.10" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, "engines": { - "node": ">= 0.8.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lilconfig": { @@ -8248,13 +7833,6 @@ "loose-envify": "cli.js" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lowlight": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", @@ -8300,13 +7878,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/markdown-extensions": { @@ -9772,6 +9350,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "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", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -9938,23 +9530,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -10939,50 +10514,39 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rollup": { - "version": "4.63.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.4.tgz", - "integrity": "sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==", + "node_modules/rolldown": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz", + "integrity": "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@types/estree": "1.0.9" + "@oxc-project/types": "=0.150.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.63.4", - "@rollup/rollup-android-arm64": "4.63.4", - "@rollup/rollup-darwin-arm64": "4.63.4", - "@rollup/rollup-darwin-x64": "4.63.4", - "@rollup/rollup-freebsd-arm64": "4.63.4", - "@rollup/rollup-freebsd-x64": "4.63.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.63.4", - "@rollup/rollup-linux-arm-musleabihf": "4.63.4", - "@rollup/rollup-linux-arm64-gnu": "4.63.4", - "@rollup/rollup-linux-arm64-musl": "4.63.4", - "@rollup/rollup-linux-loong64-gnu": "4.63.4", - "@rollup/rollup-linux-loong64-musl": "4.63.4", - "@rollup/rollup-linux-ppc64-gnu": "4.63.4", - "@rollup/rollup-linux-ppc64-musl": "4.63.4", - "@rollup/rollup-linux-riscv64-gnu": "4.63.4", - "@rollup/rollup-linux-riscv64-musl": "4.63.4", - "@rollup/rollup-linux-s390x-gnu": "4.63.4", - "@rollup/rollup-linux-x64-gnu": "4.63.4", - "@rollup/rollup-linux-x64-musl": "4.63.4", - "@rollup/rollup-openbsd-x64": "4.63.4", - "@rollup/rollup-openharmony-arm64": "4.63.4", - "@rollup/rollup-win32-arm64-msvc": "4.63.4", - "@rollup/rollup-win32-ia32-msvc": "4.63.4", - "@rollup/rollup-win32-x64-gnu": "4.63.4", - "@rollup/rollup-win32-x64-msvc": "4.63.4", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm-eabi": "1.2.9", + "@rolldown/binding-android-arm64": "1.2.9", + "@rolldown/binding-darwin-arm64": "1.2.9", + "@rolldown/binding-darwin-x64": "1.2.9", + "@rolldown/binding-freebsd-x64": "1.2.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.9", + "@rolldown/binding-linux-arm64-gnu": "1.2.9", + "@rolldown/binding-linux-arm64-musl": "1.2.9", + "@rolldown/binding-linux-ppc64-gnu": "1.2.9", + "@rolldown/binding-linux-s390x-gnu": "1.2.9", + "@rolldown/binding-linux-x64-gnu": "1.2.9", + "@rolldown/binding-linux-x64-musl": "1.2.9", + "@rolldown/binding-openharmony-arm64": "1.2.9", + "@rolldown/binding-win32-arm64-msvc": "1.2.9", + "@rolldown/binding-win32-x64-msvc": "1.2.9" } }, "node_modules/run-parallel": { @@ -11363,9 +10927,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -11566,26 +11130,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -11805,18 +11349,24 @@ "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": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "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" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/tinyglobby": { "version": "0.2.17", @@ -11863,36 +11413,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", - "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/tldts": { "version": "7.4.13", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.13.tgz", @@ -12512,18 +12032,18 @@ } }, "node_modules/vite": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", - "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "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", + "peer": true, "dependencies": { - "esbuild": "^0.27.0 || ^0.28.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -12539,9 +12059,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -12554,13 +12075,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { "optional": true }, - "lightningcss": { + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -12586,53 +12110,13 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -12641,65 +12125,72 @@ } }, "node_modules/vitest": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", - "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "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": { "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.7", - "@vitest/mocker": "3.2.7", - "@vitest/pretty-format": "^3.2.7", - "@vitest/runner": "3.2.7", - "@vitest/snapshot": "3.2.7", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", + "@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": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.7", - "@vitest/ui": "3.2.7", + "@opentelemetry/api": "^1.9.0", + "@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": "*" + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -12710,6 +12201,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, diff --git a/website/package.json b/website/package.json index a161f1b3..da318476 100644 --- a/website/package.json +++ b/website/package.json @@ -52,6 +52,6 @@ "postcss": "^8.5.3", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", - "vitest": "^3.2.7" + "vitest": "^5.0.1" } }