diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore
new file mode 100644
index 00000000000..6dd4bbef84d
--- /dev/null
+++ b/apps/mobile/.gitignore
@@ -0,0 +1,13 @@
+/node_modules
+.expo
+/ios
+/android
+dist
+coverage
+*.tsbuildinfo
+
+# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
+# The following patterns were generated by expo-cli
+
+expo-env.d.ts
+# @end expo-cli
\ No newline at end of file
diff --git a/apps/mobile/app.config.js b/apps/mobile/app.config.js
new file mode 100644
index 00000000000..7d6f2acd26f
--- /dev/null
+++ b/apps/mobile/app.config.js
@@ -0,0 +1,67 @@
+const associatedDomains =
+ process.env.CAP_MOBILE_DISABLE_ASSOCIATED_DOMAINS === "1"
+ ? []
+ : process.env.CAP_MOBILE_ASSOCIATED_DOMAINS
+ ? process.env.CAP_MOBILE_ASSOCIATED_DOMAINS.split(",")
+ .map((domain) => domain.trim())
+ .filter(Boolean)
+ : ["applinks:cap.so"];
+const bundleIdentifier = "so.cap.mobile";
+const ios = {
+ bundleIdentifier,
+ supportsTablet: false,
+ infoPlist: {
+ NSPhotoLibraryUsageDescription:
+ "Cap imports videos from Photos for upload.",
+ NSPhotoLibraryAddUsageDescription: "Cap saves downloaded videos to Photos.",
+ UIBackgroundModes: ["processing"],
+ },
+};
+
+if (associatedDomains.length > 0) {
+ ios.associatedDomains = associatedDomains;
+}
+
+module.exports = ({ config }) => ({
+ ...config,
+ name: "Cap",
+ slug: "cap-mobile",
+ scheme: "cap",
+ owner: "cap",
+ version: "0.1.0",
+ orientation: "portrait",
+ platforms: ["ios"],
+ userInterfaceStyle: "light",
+ icon: "./assets/icon.png",
+ splash: {
+ image: "./assets/splash-icon.png",
+ resizeMode: "contain",
+ backgroundColor: "#f9f9f9",
+ },
+ ios,
+ experiments: {
+ typedRoutes: true,
+ },
+ plugins: [
+ "expo-router",
+ [
+ "expo-font",
+ {
+ fonts: [
+ "../web/public/fonts/NeueMontreal-Regular.otf",
+ "../web/public/fonts/NeueMontreal-Medium.otf",
+ "../web/public/fonts/NeueMontreal-Bold.otf",
+ ],
+ },
+ ],
+ [
+ "expo-secure-store",
+ {
+ faceIDPermission: "Allow Cap to protect your account key.",
+ },
+ ],
+ ],
+ extra: {
+ apiBaseUrl: process.env.EXPO_PUBLIC_CAP_WEB_URL ?? "https://cap.so",
+ },
+});
diff --git a/apps/mobile/app/(tabs)/_layout.tsx b/apps/mobile/app/(tabs)/_layout.tsx
new file mode 100644
index 00000000000..3985c71fe94
--- /dev/null
+++ b/apps/mobile/app/(tabs)/_layout.tsx
@@ -0,0 +1,53 @@
+import { NativeTabs } from "expo-router/unstable-native-tabs";
+import { colors, fonts } from "@/theme";
+
+export default function TabsLayout() {
+ return (
+
+
+ My Caps
+
+
+
+ Import
+
+
+
+ Account
+
+
+
+ );
+}
diff --git a/apps/mobile/app/(tabs)/account.tsx b/apps/mobile/app/(tabs)/account.tsx
new file mode 100644
index 00000000000..7bb19f2b05e
--- /dev/null
+++ b/apps/mobile/app/(tabs)/account.tsx
@@ -0,0 +1,495 @@
+import Constants from "expo-constants";
+import { Image } from "expo-image";
+import { type SFSymbol, SymbolView } from "expo-symbols";
+import * as WebBrowser from "expo-web-browser";
+import { type ReactNode, useRef, useState } from "react";
+import {
+ ActionSheetIOS,
+ ActivityIndicator,
+ Alert,
+ Linking,
+ Platform,
+ Pressable,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+import { apiBaseUrl, useAuth } from "@/auth/AuthContext";
+import { SignInPanel } from "@/auth/SignInPanel";
+import { GlassSurface } from "@/components/GlassSurface";
+import { OrgSwitcher } from "@/components/OrgSwitcher";
+import { Screen } from "@/components/Screen";
+import { colors, fonts, radius, squircle } from "@/theme";
+
+type SettingsRowProps = {
+ label: string;
+ symbol: SFSymbol;
+ onPress?: () => void;
+ tintColor?: string;
+ destructive?: boolean;
+ value?: string;
+ accessibilityValueText?: string;
+ showChevron?: boolean;
+ accessibilityHint?: string;
+ busy?: boolean;
+ disabled?: boolean;
+};
+
+function SettingsRow({
+ label,
+ symbol,
+ onPress,
+ tintColor = colors.gray12,
+ destructive = false,
+ value,
+ accessibilityValueText,
+ showChevron = true,
+ accessibilityHint,
+ busy = false,
+ disabled = false,
+}: SettingsRowProps) {
+ const accessibilityValue = accessibilityValueText
+ ? { text: accessibilityValueText }
+ : value
+ ? { text: value }
+ : undefined;
+ const isAction = Boolean(onPress);
+ const isDisabled = disabled || busy;
+ const content = (
+ <>
+
+ {busy ? (
+
+ ) : (
+
+ )}
+
+
+ {label}
+
+ {value ? (
+
+ {value}
+
+ ) : null}
+ {showChevron && isAction ? (
+
+ ) : null}
+ >
+ );
+
+ if (!onPress) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return (
+ [
+ styles.settingsRow,
+ isDisabled && styles.settingsRowDisabled,
+ pressed && !isDisabled && styles.pressed,
+ ]}
+ >
+ {content}
+
+ );
+}
+
+function SettingsSection({
+ children,
+ title,
+}: {
+ children: ReactNode;
+ title: string;
+}) {
+ return (
+
+ {title}
+
+ {children}
+
+
+ );
+}
+
+type AccountAction =
+ | "appSettings"
+ | "organizationSettings"
+ | "refresh"
+ | "signOut";
+
+export default function AccountScreen() {
+ const auth = useAuth();
+ const appVersion = Constants.expoConfig?.version ?? "0.1.0";
+ const [accountAction, setAccountAction] = useState(
+ null,
+ );
+ const accountActionRef = useRef(null);
+ const accountActionHint =
+ accountAction === "refresh"
+ ? "Refresh is in progress"
+ : accountAction === "signOut"
+ ? "Sign out is in progress"
+ : accountAction !== null
+ ? "Settings are opening"
+ : null;
+ const accountActionDisabled = accountAction !== null;
+
+ const runAccountAction = async (
+ action: AccountAction,
+ operation: () => Promise,
+ ) => {
+ if (accountActionRef.current !== null) return;
+ accountActionRef.current = action;
+ setAccountAction(action);
+ try {
+ await operation();
+ } finally {
+ accountActionRef.current = null;
+ setAccountAction(null);
+ }
+ };
+
+ const confirmSignOut = () => {
+ if (accountActionRef.current !== null) return;
+
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: 1,
+ destructiveButtonIndex: 0,
+ message: "Remove this Cap session from your device?",
+ options: ["Sign out", "Cancel"],
+ title: "Sign out",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) {
+ void runAccountAction("signOut", auth.signOut);
+ }
+ },
+ );
+ return;
+ }
+
+ Alert.alert("Sign out", "Remove this Cap session from your device?", [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Sign out",
+ style: "destructive",
+ onPress: () => {
+ void runAccountAction("signOut", auth.signOut);
+ },
+ },
+ ]);
+ };
+
+ if (auth.status === "loading") {
+ return ;
+ }
+
+ if (auth.status === "signedOut") {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {auth.bootstrap ? (
+
+
+
+ {auth.bootstrap.user.imageUrl ? (
+
+ ) : (
+
+ {(auth.bootstrap.user.name ?? auth.bootstrap.user.email)
+ .slice(0, 1)
+ .toUpperCase()}
+
+ )}
+
+
+
+ {auth.bootstrap.user.name ?? "Cap user"}
+
+
+ {auth.bootstrap.user.email}
+
+
+
+
+
+ ) : null}
+
+ {
+ void runAccountAction("organizationSettings", () =>
+ WebBrowser.openBrowserAsync(
+ new URL(
+ "/dashboard/settings/organization",
+ apiBaseUrl,
+ ).toString(),
+ ),
+ );
+ }}
+ value={
+ accountAction === "organizationSettings" ? "Opening..." : undefined
+ }
+ />
+
+
+ {
+ void runAccountAction("refresh", auth.refresh);
+ }}
+ value={accountAction === "refresh" ? "Refreshing..." : undefined}
+ />
+
+ {
+ void runAccountAction("appSettings", Linking.openSettings);
+ }}
+ value={accountAction === "appSettings" ? "Opening..." : undefined}
+ />
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ card: {
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ padding: 16,
+ gap: 16,
+ ...squircle,
+ },
+ cardFallback: {
+ backgroundColor: colors.gray1,
+ },
+ identityRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 12,
+ },
+ avatar: {
+ width: 48,
+ height: 48,
+ borderRadius: radius.sm,
+ overflow: "hidden",
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.blue3,
+ ...squircle,
+ },
+ avatarImage: {
+ width: "100%",
+ height: "100%",
+ },
+ avatarText: {
+ fontFamily: fonts.medium,
+ fontSize: 18,
+ color: colors.blue11,
+ },
+ identityText: {
+ flex: 1,
+ minWidth: 0,
+ },
+ name: {
+ fontFamily: fonts.medium,
+ fontSize: 19,
+ color: colors.gray12,
+ },
+ email: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ color: colors.gray10,
+ marginTop: 2,
+ },
+ settingsGroup: {
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ overflow: "hidden",
+ ...squircle,
+ },
+ section: {
+ marginTop: 16,
+ gap: 8,
+ },
+ sectionTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 13,
+ lineHeight: 18,
+ color: colors.gray10,
+ paddingHorizontal: 4,
+ },
+ settingsFallback: {
+ backgroundColor: colors.gray1,
+ },
+ settingsRow: {
+ minHeight: 54,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 12,
+ paddingHorizontal: 14,
+ },
+ settingsRowDisabled: {
+ backgroundColor: colors.gray2,
+ },
+ pressed: {
+ backgroundColor: colors.gray3,
+ },
+ settingsIcon: {
+ width: 30,
+ height: 30,
+ borderRadius: radius.sm,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.gray3,
+ ...squircle,
+ },
+ settingsLabel: {
+ flex: 1,
+ fontFamily: fonts.medium,
+ fontSize: 16,
+ color: colors.gray12,
+ },
+ settingsLabelDisabled: {
+ color: colors.gray9,
+ },
+ settingsValue: {
+ fontFamily: fonts.regular,
+ fontSize: 15,
+ color: colors.gray10,
+ },
+ settingsValueDisabled: {
+ color: colors.gray9,
+ },
+ dangerLabel: {
+ color: colors.red9,
+ },
+ separator: {
+ height: StyleSheet.hairlineWidth,
+ backgroundColor: colors.gray4,
+ marginLeft: 56,
+ },
+});
diff --git a/apps/mobile/app/(tabs)/index.tsx b/apps/mobile/app/(tabs)/index.tsx
new file mode 100644
index 00000000000..817f7e3c7f2
--- /dev/null
+++ b/apps/mobile/app/(tabs)/index.tsx
@@ -0,0 +1,947 @@
+import { FlashList } from "@shopify/flash-list";
+import * as Clipboard from "expo-clipboard";
+import { router } from "expo-router";
+import { SymbolView } from "expo-symbols";
+import * as WebBrowser from "expo-web-browser";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import {
+ ActionSheetIOS,
+ Alert,
+ Linking,
+ Platform,
+ Pressable,
+ Share,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+import type {
+ MobileCapSummary,
+ MobileCapsListResponse,
+ MobileFolder,
+} from "@/api/mobile";
+import { MobileApiError } from "@/api/mobile";
+import { apiBaseUrl, useAuth } from "@/auth/AuthContext";
+import { SignInPanel } from "@/auth/SignInPanel";
+import { CapSettingsSheet } from "@/caps/CapSettingsSheet";
+import { showCapPasswordActions } from "@/caps/passwordActions";
+import {
+ PhotosPermissionDeniedError,
+ saveCapVideoToPhotos,
+} from "@/caps/saveCapVideo";
+import { showCapTitleActions } from "@/caps/titleActions";
+import { ActionButton } from "@/components/ActionButton";
+import { CapCard } from "@/components/CapCard";
+import { CapLogoBadge } from "@/components/CapLogoBadge";
+import { CapRefreshControl } from "@/components/CapRefreshControl";
+import { OrgSwitcher } from "@/components/OrgSwitcher";
+import { Screen } from "@/components/Screen";
+import { colors, fonts, radius, squircle } from "@/theme";
+
+type ListItem =
+ | { type: "section"; id: "folders" | "videos"; title: string }
+ | { type: "folder"; folder: MobileFolder }
+ | { type: "cap"; cap: MobileCapSummary };
+
+const folderColorOptions: Array<{
+ label: string;
+ color: MobileFolder["color"];
+}> = [
+ { label: "Normal", color: "normal" },
+ { label: "Blue", color: "blue" },
+ { label: "Red", color: "red" },
+ { label: "Yellow", color: "yellow" },
+];
+
+const folderTintByColor = {
+ normal: colors.gray12,
+ blue: colors.blue9,
+ red: colors.red9,
+ yellow: colors.yellow9,
+} as const;
+
+const getCapsErrorMessage = (error: unknown) => {
+ if (error instanceof MobileApiError) {
+ if (error.status === 401) return "Your session expired. Sign in again.";
+ return "Cap could not load your library. Try again.";
+ }
+ return error instanceof Error
+ ? error.message
+ : "Cap could not load your library. Try again.";
+};
+
+const showPhotosSettingsAlert = () => {
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: 1,
+ message: "Allow Cap to save videos to Photos from Settings.",
+ options: ["Open Settings", "Cancel"],
+ title: "Photos access needed",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) void Linking.openSettings();
+ },
+ );
+ return;
+ }
+
+ Alert.alert(
+ "Photos access needed",
+ "Allow Cap to save videos to Photos from Settings.",
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Open Settings",
+ onPress: () => {
+ void Linking.openSettings();
+ },
+ },
+ ],
+ );
+};
+
+export default function CapsScreen() {
+ const auth = useAuth();
+ const [folder, setFolder] = useState(null);
+ const [result, setResult] = useState(null);
+ const [refreshing, setRefreshing] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [loadError, setLoadError] = useState(null);
+ const [savingId, setSavingId] = useState(null);
+ const [updatingSharingId, setUpdatingSharingId] = useState(
+ null,
+ );
+ const [settingsCap, setSettingsCap] = useState(null);
+ const [creatingFolder, setCreatingFolder] = useState(false);
+ const [creatingFolderName, setCreatingFolderName] = useState(
+ null,
+ );
+
+ const load = useCallback(async () => {
+ if (auth.status !== "signedIn") return;
+ setLoading(true);
+ try {
+ const response = await auth.client.listCaps({
+ folderId: folder?.id ?? null,
+ page: 1,
+ limit: 30,
+ });
+ setResult(response);
+ setLoadError(null);
+ } catch (error) {
+ setLoadError(getCapsErrorMessage(error));
+ } finally {
+ setLoading(false);
+ }
+ }, [auth, folder?.id]);
+
+ useEffect(() => {
+ void load();
+ }, [load]);
+
+ const refresh = useCallback(async () => {
+ setRefreshing(true);
+ try {
+ await Promise.all([auth.refresh(), load()]);
+ } catch (error) {
+ setLoadError(getCapsErrorMessage(error));
+ } finally {
+ setRefreshing(false);
+ }
+ }, [auth, load]);
+
+ const confirmDeleteCap = useCallback(
+ (cap: MobileCapSummary) => {
+ if (auth.status !== "signedIn") return;
+ const deleteCap = () => {
+ void (async () => {
+ setSettingsCap(null);
+ await auth.client.deleteCap(cap.id);
+ await Promise.all([auth.refresh(), load()]);
+ })();
+ };
+
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: 1,
+ destructiveButtonIndex: 0,
+ message: `${cap.title} will be removed from your library.`,
+ options: ["Delete Cap", "Cancel"],
+ title: "Delete Cap",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) deleteCap();
+ },
+ );
+ return;
+ }
+
+ Alert.alert(
+ "Delete Cap",
+ `${cap.title} will be removed from your library.`,
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Delete",
+ style: "destructive",
+ onPress: deleteCap,
+ },
+ ],
+ );
+ },
+ [auth, load],
+ );
+
+ const copyCapLink = useCallback((cap: MobileCapSummary) => {
+ void Clipboard.setStringAsync(cap.shareUrl);
+ }, []);
+
+ const shareCapLink = useCallback((cap: MobileCapSummary) => {
+ void Share.share({ url: cap.shareUrl, message: cap.shareUrl });
+ }, []);
+
+ const updateCapVisibility = useCallback(
+ async (cap: MobileCapSummary, isPublic: boolean) => {
+ if (auth.status !== "signedIn" || updatingSharingId !== null) return;
+ setUpdatingSharingId(cap.id);
+ try {
+ const updated = await auth.client.updateCapSharing(cap.id, {
+ public: isPublic,
+ });
+ setSettingsCap((current) =>
+ current?.id === updated.id ? updated : current,
+ );
+ await Promise.all([auth.refresh(), load()]);
+ } catch (error) {
+ Alert.alert(
+ "Sharing update failed",
+ error instanceof Error
+ ? error.message
+ : "Unable to update sharing for this Cap.",
+ );
+ } finally {
+ setUpdatingSharingId(null);
+ }
+ },
+ [auth, load, updatingSharingId],
+ );
+
+ const saveCapVideo = useCallback(
+ async (cap: MobileCapSummary) => {
+ if (auth.status !== "signedIn" || savingId !== null) return;
+ setSavingId(cap.id);
+ try {
+ await saveCapVideoToPhotos(auth.client, cap.id);
+ } catch (error) {
+ if (error instanceof PhotosPermissionDeniedError) {
+ showPhotosSettingsAlert();
+ return;
+ }
+ Alert.alert(
+ "Save failed",
+ error instanceof Error ? error.message : "Unable to save this video.",
+ );
+ } finally {
+ setSavingId(null);
+ }
+ },
+ [auth, savingId],
+ );
+
+ const showPasswordActions = useCallback(
+ (cap: MobileCapSummary) => {
+ if (auth.status !== "signedIn") return;
+ showCapPasswordActions({
+ cap,
+ client: auth.client,
+ onUpdated: async (updated) => {
+ setSettingsCap((current) =>
+ current?.id === updated.id ? updated : current,
+ );
+ await Promise.all([auth.refresh(), load()]);
+ },
+ });
+ },
+ [auth, load],
+ );
+
+ const showTitleActions = useCallback(
+ (cap: MobileCapSummary) => {
+ if (auth.status !== "signedIn") return;
+ showCapTitleActions({
+ cap,
+ client: auth.client,
+ onUpdated: async (updated) => {
+ setSettingsCap((current) =>
+ current?.id === updated.id ? updated : current,
+ );
+ await Promise.all([auth.refresh(), load()]);
+ },
+ });
+ },
+ [auth, load],
+ );
+
+ const showCapSettings = useCallback((cap: MobileCapSummary) => {
+ setSettingsCap(cap);
+ }, []);
+
+ const viewAnalytics = useCallback((cap: MobileCapSummary) => {
+ const url = new URL("/dashboard/analytics", apiBaseUrl);
+ url.searchParams.set("capId", cap.id);
+ void WebBrowser.openBrowserAsync(url.toString());
+ }, []);
+
+ const createFolder = useCallback(
+ async (name: string, color: MobileFolder["color"]) => {
+ if (auth.status !== "signedIn" || creatingFolder) return;
+ const trimmedName = name.trim();
+ if (!trimmedName) {
+ Alert.alert("Folder name required", "Enter a folder name to continue.");
+ return;
+ }
+
+ setCreatingFolder(true);
+ setCreatingFolderName(trimmedName);
+ try {
+ await auth.client.createFolder({ name: trimmedName, color });
+ setFolder(null);
+ await Promise.all([auth.refresh(), load()]);
+ } catch (error) {
+ Alert.alert(
+ "Folder creation failed",
+ error instanceof Error
+ ? error.message
+ : "Unable to create this folder.",
+ );
+ } finally {
+ setCreatingFolder(false);
+ setCreatingFolderName(null);
+ }
+ },
+ [auth, creatingFolder, load],
+ );
+
+ const showFolderColorSheet = useCallback(
+ (name: string) => {
+ if (Platform.OS !== "ios") {
+ void createFolder(name, "normal");
+ return;
+ }
+
+ const cancelButtonIndex = folderColorOptions.length;
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex,
+ message: name,
+ options: [
+ ...folderColorOptions.map((option) => option.label),
+ "Cancel",
+ ],
+ title: "Folder color",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ const option = folderColorOptions[index];
+ if (option) void createFolder(name, option.color);
+ },
+ );
+ },
+ [createFolder],
+ );
+
+ const showNewFolderPrompt = useCallback(() => {
+ if (auth.status !== "signedIn" || creatingFolder) return;
+
+ if (Platform.OS === "ios") {
+ Alert.prompt(
+ "New Folder",
+ "Name this folder.",
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Next",
+ onPress: (value?: string) => {
+ const name = value?.trim() ?? "";
+ if (!name) {
+ Alert.alert(
+ "Folder name required",
+ "Enter a folder name to continue.",
+ );
+ return;
+ }
+ showFolderColorSheet(name);
+ },
+ },
+ ],
+ "plain-text",
+ );
+ return;
+ }
+
+ Alert.alert("New Folder", "Create a folder named Untitled?", [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Create",
+ onPress: () => {
+ void createFolder("Untitled", "normal");
+ },
+ },
+ ]);
+ }, [auth.status, createFolder, creatingFolder, showFolderColorSheet]);
+
+ const showSharingActions = useCallback(
+ (cap: MobileCapSummary) => {
+ if (updatingSharingId !== null) return;
+ const visibilityAction = cap.public ? "Make private" : "Make public";
+
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: 3,
+ message: cap.shareUrl,
+ options: [visibilityAction, "Copy link", "Share link", "Cancel"],
+ title: cap.public ? "Shared" : "Not shared",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) void updateCapVisibility(cap, !cap.public);
+ if (index === 1) copyCapLink(cap);
+ if (index === 2) shareCapLink(cap);
+ },
+ );
+ return;
+ }
+
+ Alert.alert(cap.public ? "Shared" : "Not shared", cap.shareUrl, [
+ {
+ text: visibilityAction,
+ onPress: () => void updateCapVisibility(cap, !cap.public),
+ },
+ { text: "Copy link", onPress: () => copyCapLink(cap) },
+ { text: "Share link", onPress: () => shareCapLink(cap) },
+ { text: "Cancel", style: "cancel" },
+ ]);
+ },
+ [copyCapLink, shareCapLink, updateCapVisibility, updatingSharingId],
+ );
+
+ const items = useMemo(() => {
+ if (!result) return [];
+ const nextItems: ListItem[] = [];
+ if (result.folders.length > 0) {
+ nextItems.push({ type: "section", id: "folders", title: "Folders" });
+ nextItems.push(
+ ...result.folders.map((item) => ({
+ type: "folder" as const,
+ folder: item,
+ })),
+ );
+ }
+ if (result.caps.length > 0) {
+ nextItems.push({ type: "section", id: "videos", title: "Videos" });
+ nextItems.push(
+ ...result.caps.map((item) => ({ type: "cap" as const, cap: item })),
+ );
+ }
+ return nextItems;
+ }, [result]);
+
+ const userName = auth.bootstrap?.user.name?.split(" ")[0];
+ const folderCreationHint = creatingFolder
+ ? "Folder creation is in progress"
+ : "Creates a folder for organizing Caps";
+ const folderCreationStatus = creatingFolder
+ ? `Creating folder ${creatingFolderName ?? ""}`.trim()
+ : null;
+ const folderCreationAccessibilityLabel = "New Folder";
+ const folderCreationAccessibilityValue = folderCreationStatus
+ ? { text: folderCreationStatus }
+ : undefined;
+ const dashboardActionHint = creatingFolder
+ ? "Folder creation is in progress"
+ : null;
+ const savingCap =
+ savingId !== null
+ ? settingsCap?.id === savingId
+ ? settingsCap
+ : (result?.caps.find((cap) => cap.id === savingId) ?? null)
+ : null;
+ const updatingSharingCap =
+ updatingSharingId !== null
+ ? settingsCap?.id === updatingSharingId
+ ? settingsCap
+ : (result?.caps.find((cap) => cap.id === updatingSharingId) ?? null)
+ : null;
+ const isLibraryActionInProgress =
+ savingId !== null || updatingSharingId !== null;
+ const saveDisabledHint =
+ savingId !== null
+ ? "Save is in progress"
+ : "Current Cap action is in progress";
+ const visibilityDisabledHint =
+ updatingSharingId !== null
+ ? "Sharing update is in progress"
+ : "Current Cap action is in progress";
+ const saveDisabledAccessibilityValue = savingCap
+ ? `Saving video for ${savingCap.title}`
+ : undefined;
+ const visibilityDisabledAccessibilityValue = updatingSharingCap
+ ? `Updating sharing for ${updatingSharingCap.title}`
+ : undefined;
+
+ if (auth.status === "loading") {
+ return ;
+ }
+
+ if (auth.status === "signedOut") {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {auth.bootstrap ? (
+
+ {
+ setFolder(null);
+ await auth.setActiveOrganization(organizationId);
+ await load();
+ }}
+ />
+
+ ) : null}
+
+
+ router.push("/upload")}
+ disabled={creatingFolder}
+ size="sm"
+ style={styles.actionButton}
+ symbol="square.and.arrow.up"
+ variant="dark"
+ />
+
+ {folder ? (
+ setFolder(null)}
+ style={styles.folderCrumb}
+ >
+ My Caps
+
+
+
+
+
+ {folder.name}
+
+
+ ) : null}
+ {loadError ? (
+
+
+
+
+
+ Unable to load Caps
+ {loadError}
+
+
+
+ ) : null}
+ {loadError && !result ? null : (
+
+ item.type === "section"
+ ? `section-${item.id}`
+ : item.type === "folder"
+ ? `folder-${item.folder.id}`
+ : `cap-${item.cap.id}`
+ }
+ refreshControl={
+
+ }
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={styles.listContent}
+ getItemType={(item) => item.type}
+ ListEmptyComponent={
+
+
+
+
+
+
+
+
+
+ Hey{userName ? ` ${userName}` : ""}! Import your first Cap
+
+
+ Bring videos into Cap and share them instantly.
+
+
+ router.push("/upload")}
+ disabled={creatingFolder}
+ style={styles.emptyButton}
+ symbol="square.and.arrow.up"
+ variant="dark"
+ />
+
+
+ }
+ renderItem={({ item }) =>
+ item.type === "section" ? (
+
+ {item.title}
+
+ ) : item.type === "folder" ? (
+ setFolder(item.folder)}
+ style={({ pressed }) => [
+ styles.folderRow,
+ pressed ? styles.folderRowPressed : null,
+ ]}
+ >
+
+
+
+
+
+ {item.folder.name}
+
+
+ {item.folder.videoCount}{" "}
+ {item.folder.videoCount === 1 ? "video" : "videos"}
+
+
+
+
+ ) : (
+ viewAnalytics(item.cap)}
+ onCopyPress={() => copyCapLink(item.cap)}
+ onPress={() => router.push(`/caps/${item.cap.id}`)}
+ onSharePress={() => shareCapLink(item.cap)}
+ onVisibilityPress={() => showSharingActions(item.cap)}
+ onMenuPress={() => showCapSettings(item.cap)}
+ visibilityBusy={updatingSharingId === item.cap.id}
+ visibilityDisabled={updatingSharingId !== null}
+ visibilityDisabledHint={
+ updatingSharingId === item.cap.id
+ ? "Sharing update is in progress"
+ : "Another sharing update is in progress"
+ }
+ visibilityAccessibilityValue={
+ updatingSharingId === item.cap.id
+ ? `Updating sharing for ${item.cap.title}`
+ : undefined
+ }
+ />
+ )
+ }
+ />
+ )}
+ setSettingsCap(null)}
+ onCopyLink={copyCapLink}
+ onDelete={confirmDeleteCap}
+ onPassword={showPasswordActions}
+ onRename={showTitleActions}
+ onSaveVideo={(cap) => {
+ void saveCapVideo(cap);
+ }}
+ onShareLink={shareCapLink}
+ onViewAnalytics={viewAnalytics}
+ onVisibilityChange={(cap, isPublic) => {
+ void updateCapVisibility(cap, isPublic);
+ }}
+ saveDisabled={isLibraryActionInProgress}
+ saveDisabledHint={saveDisabledHint}
+ saveDisabledValue={savingId !== null ? undefined : "Unavailable"}
+ saveDisabledAccessibilityValue={saveDisabledAccessibilityValue}
+ visibilityDisabled={isLibraryActionInProgress}
+ visibilityDisabledHint={visibilityDisabledHint}
+ visibilityDisabledAccessibilityValue={
+ visibilityDisabledAccessibilityValue
+ }
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ topBar: {
+ marginBottom: 12,
+ },
+ actions: {
+ flexDirection: "row",
+ flexWrap: "wrap",
+ gap: 8,
+ marginBottom: 40,
+ },
+ actionButton: {
+ flexGrow: 1,
+ flexBasis: 104,
+ paddingHorizontal: 12,
+ },
+ listContent: {
+ paddingBottom: 22,
+ },
+ folderCrumb: {
+ minHeight: 40,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 7,
+ marginBottom: 14,
+ },
+ folderCrumbText: {
+ fontFamily: fonts.medium,
+ color: colors.gray9,
+ fontSize: 20,
+ lineHeight: 26,
+ },
+ folderCrumbIcon: {
+ width: 24,
+ height: 24,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ folderCurrent: {
+ flex: 1,
+ fontFamily: fonts.medium,
+ color: colors.gray12,
+ fontSize: 20,
+ lineHeight: 26,
+ },
+ folderRow: {
+ minHeight: 82,
+ flexDirection: "row",
+ alignItems: "center",
+ borderRadius: radius.sm,
+ borderWidth: StyleSheet.hairlineWidth,
+ paddingHorizontal: 16,
+ paddingVertical: 16,
+ gap: 12,
+ marginBottom: 12,
+ backgroundColor: colors.gray3,
+ borderColor: colors.gray5,
+ ...squircle,
+ },
+ folderRowPressed: {
+ backgroundColor: colors.gray4,
+ borderColor: colors.gray6,
+ },
+ sectionHeader: {
+ paddingTop: 8,
+ paddingBottom: 24,
+ },
+ sectionTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 24,
+ lineHeight: 30,
+ color: colors.gray12,
+ },
+ folderIcon: {
+ width: 50,
+ height: 50,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ folderText: {
+ flex: 1,
+ minWidth: 0,
+ },
+ folderName: {
+ fontFamily: fonts.regular,
+ fontSize: 15,
+ lineHeight: 22,
+ color: colors.gray12,
+ },
+ folderMeta: {
+ fontFamily: fonts.regular,
+ fontSize: 13,
+ lineHeight: 18,
+ color: colors.gray10,
+ },
+ errorCard: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 12,
+ backgroundColor: colors.gray1,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ padding: 14,
+ marginBottom: 14,
+ ...squircle,
+ },
+ errorIcon: {
+ width: 36,
+ height: 36,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.gray3,
+ ...squircle,
+ },
+ errorCopy: {
+ flex: 1,
+ minWidth: 0,
+ },
+ errorTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 15,
+ lineHeight: 20,
+ color: colors.gray12,
+ },
+ errorText: {
+ fontFamily: fonts.regular,
+ fontSize: 13,
+ lineHeight: 18,
+ color: colors.gray10,
+ marginTop: 2,
+ },
+ errorButton: {
+ paddingHorizontal: 14,
+ },
+ emptyState: {
+ alignItems: "center",
+ paddingTop: 42,
+ gap: 12,
+ paddingHorizontal: 8,
+ },
+ emptyArt: {
+ width: 180,
+ height: 112,
+ alignItems: "center",
+ justifyContent: "center",
+ marginBottom: 10,
+ },
+ emptyArtCard: {
+ position: "absolute",
+ width: 152,
+ height: 86,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ backgroundColor: colors.gray1,
+ transform: [{ rotate: "-4deg" }],
+ ...squircle,
+ },
+ emptyArtCardBack: {
+ backgroundColor: colors.gray3,
+ borderColor: colors.gray4,
+ transform: [{ translateX: 12 }, { translateY: 7 }, { rotate: "5deg" }],
+ },
+ emptyLogo: {
+ width: 72,
+ height: 72,
+ borderRadius: radius.lg,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.white,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ ...squircle,
+ },
+ emptyTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 20,
+ color: colors.gray12,
+ textAlign: "center",
+ },
+ emptyText: {
+ fontFamily: fonts.regular,
+ fontSize: 15,
+ lineHeight: 22,
+ color: colors.gray10,
+ textAlign: "center",
+ },
+ emptyActions: {
+ width: "100%",
+ flexDirection: "row",
+ gap: 10,
+ marginTop: 4,
+ },
+ emptyButton: {
+ flex: 1,
+ },
+});
diff --git a/apps/mobile/app/(tabs)/upload.tsx b/apps/mobile/app/(tabs)/upload.tsx
new file mode 100644
index 00000000000..aa790e62a9e
--- /dev/null
+++ b/apps/mobile/app/(tabs)/upload.tsx
@@ -0,0 +1,1254 @@
+import * as DocumentPicker from "expo-document-picker";
+import * as ImagePicker from "expo-image-picker";
+import { router } from "expo-router";
+import { SymbolView } from "expo-symbols";
+import * as WebBrowser from "expo-web-browser";
+import { useEffect, useReducer, useRef, useState } from "react";
+import {
+ ActionSheetIOS,
+ ActivityIndicator,
+ Alert,
+ Linking,
+ Platform,
+ Pressable,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+import Svg, { Path } from "react-native-svg";
+import type { UploadFile } from "@/api/mobile";
+import { apiBaseUrl, useAuth } from "@/auth/AuthContext";
+import { SignInPanel } from "@/auth/SignInPanel";
+import { ActionButton } from "@/components/ActionButton";
+import { GlassSurface } from "@/components/GlassSurface";
+import { Screen } from "@/components/Screen";
+import { colors, fonts, radius, squircle } from "@/theme";
+import { contentTypeForUpload } from "@/uploads/fileTypes";
+import { runMobileUpload } from "@/uploads/runMobileUpload";
+import {
+ emptyUploadQueue,
+ isTerminalUploadQueueAction,
+ type UploadQueueItem,
+ uploadProgressPercent,
+ uploadQueueActionFromCapUpload,
+ uploadQueueReducer,
+ uploadQueueStatusText,
+} from "@/uploads/uploadQueue";
+import { formatDuration, formatFileSize } from "@/utils/format";
+
+const processingPollDelaysMs = [1500, 3000, 5000, 8000] as const;
+const photosAccessNeededMessage =
+ "Allow Cap to read videos from Photos before uploading.";
+const uploadAcceptedFormats = "MP4, MOV, AVI, MKV, WebM, or M4V";
+type UploadSourceLoading = "files" | "loom" | "photos" | null;
+type UploadSource = Exclude;
+type UploadSourceError = {
+ message: string;
+ source: UploadSource;
+};
+
+const queueItemFromFile = (
+ file: UploadFile,
+ organizationId: string | null,
+): Omit => ({
+ id: `${Date.now()}-${file.name}`,
+ localUri: file.uri,
+ fileName: file.name,
+ contentType: file.type,
+ size: file.size ?? 0,
+ durationSeconds: file.durationSeconds,
+ width: file.width,
+ height: file.height,
+ folderId: null,
+ organizationId,
+ status: "queued",
+ progress: 0,
+ error: null,
+ capId: null,
+ rawFileKey: null,
+ processingMessage: null,
+});
+
+const showPhotosSettingsAlert = () => {
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: 1,
+ message: photosAccessNeededMessage,
+ options: ["Open Settings", "Cancel"],
+ title: "Photos access needed",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) void Linking.openSettings();
+ },
+ );
+ return;
+ }
+
+ Alert.alert("Photos access needed", photosAccessNeededMessage, [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Open Settings",
+ onPress: () => {
+ void Linking.openSettings();
+ },
+ },
+ ]);
+};
+
+const getUploadSourceErrorMessage = (error: unknown, source: UploadSource) =>
+ error instanceof Error
+ ? error.message
+ : source === "loom"
+ ? "Unable to open Loom import"
+ : "Unable to open the picker";
+
+const uploadQueueMetadataText = (
+ item: UploadQueueItem,
+ statusText = uploadQueueStatusText(item),
+) => {
+ const failureReason =
+ item.status === "failed" && item.error?.trim() ? item.error.trim() : null;
+ return [
+ statusText,
+ failureReason,
+ formatFileSize(item.size),
+ formatDuration(item.durationSeconds ?? null),
+ ]
+ .filter(Boolean)
+ .join(" ยท ");
+};
+
+const uploadQueueMenuHint = (item: UploadQueueItem) => {
+ if (item.status === "failed") return "Opens retry and remove actions";
+ if (
+ (item.status === "processing" || item.status === "complete") &&
+ item.capId
+ ) {
+ return "Opens view and remove actions";
+ }
+ return "Opens remove action";
+};
+
+const uploadQueueHasProgress = (item: UploadQueueItem) =>
+ item.status === "uploading" ||
+ item.status === "processing" ||
+ item.status === "complete";
+
+const progressAccessibilityValue = (percent: number) => ({
+ max: 100,
+ min: 0,
+ now: percent,
+ text: `${percent}%`,
+});
+
+const LoomMark = () => (
+
+);
+
+const idleLoomImportLabel = "Import from Loom";
+
+export default function UploadScreen() {
+ const auth = useAuth();
+ const [queue, dispatch] = useReducer(uploadQueueReducer, emptyUploadQueue);
+ const [activeId, setActiveId] = useState(null);
+ const [activeUploadName, setActiveUploadName] = useState(null);
+ const [sourceError, setSourceError] = useState(
+ null,
+ );
+ const [sourceLoading, setSourceLoading] = useState(null);
+ const mountedRef = useRef(true);
+ const activeIdRef = useRef(null);
+ const sourceBusyRef = useRef(false);
+ const uploadSourceError =
+ sourceError?.source === "files" || sourceError?.source === "photos"
+ ? sourceError.message
+ : null;
+ const loomImportError =
+ sourceError?.source === "loom" ? sourceError.message : null;
+ const activeItem = activeId
+ ? (queue.items.find((item) => item.id === activeId) ?? null)
+ : null;
+ const activeUploadFileName = activeItem?.fileName ?? activeUploadName;
+ const activeUploadPreparing =
+ activeId !== null &&
+ (activeItem === null || activeItem.status === "queued");
+ const activeProgress =
+ activeItem !== null && uploadQueueHasProgress(activeItem)
+ ? uploadProgressPercent(activeItem.progress)
+ : null;
+ const activeUploadHint = activeUploadPreparing
+ ? "Preparing upload"
+ : "Upload is in progress";
+ const uploadSourceBusy = activeId !== null || sourceLoading !== null;
+ const sourcePending =
+ sourceLoading !== null && sourceLoading !== "loom" && activeId === null;
+ const sourceLoadingTitle =
+ sourcePending && sourceLoading === "files"
+ ? "Opening Files"
+ : sourcePending && sourceLoading === "photos"
+ ? "Opening Photos"
+ : null;
+ const sourceLoadingSubtitle =
+ sourcePending && sourceLoading === "files"
+ ? "Choose a video from Files."
+ : sourcePending && sourceLoading === "photos"
+ ? "Choose a video from Photos."
+ : null;
+ const sourceLoadingAccessibilityText =
+ sourceLoading === "files"
+ ? "Opening native file picker"
+ : sourceLoading === "photos"
+ ? "Opening native photo picker"
+ : sourceLoading === "loom"
+ ? "Opening Loom import"
+ : null;
+ const importTitle = sourceLoadingTitle ?? "Upload File";
+ const importSubtitle =
+ uploadSourceError ??
+ sourceLoadingSubtitle ??
+ (activeId !== null
+ ? activeUploadPreparing
+ ? "Preparing your video for upload."
+ : "Keep Cap open while your video uploads."
+ : "Upload a video file from your device");
+ const loomImportTitle = loomImportError
+ ? "Loom import unavailable"
+ : sourceLoading === "loom"
+ ? "Opening Loom"
+ : idleLoomImportLabel;
+ const loomImportSubtitle =
+ loomImportError ??
+ (sourceLoading === "loom"
+ ? "Continue in the browser sheet to import from Loom."
+ : activeId !== null
+ ? activeUploadPreparing
+ ? "Finish preparing this upload before importing from Loom."
+ : "Finish the current upload before importing from Loom."
+ : "Import a Loom share link or bulk import from CSV");
+ const activeUploadAccessibilityLabel = activeUploadFileName
+ ? activeUploadPreparing
+ ? `Preparing upload ${activeUploadFileName}`
+ : activeProgress !== null
+ ? `Uploading ${activeUploadFileName} ${activeProgress}%`
+ : `Uploading ${activeUploadFileName}`
+ : null;
+ const activeUploadAccessibilityValue = activeUploadAccessibilityLabel
+ ? { text: activeUploadAccessibilityLabel }
+ : undefined;
+ const showUploadFormats =
+ !uploadSourceError && !sourcePending && activeId === null;
+ const uploadSourceAccessibilityLabel = sourcePending
+ ? (sourceLoadingTitle ?? "Upload source opening")
+ : uploadSourceError
+ ? "Upload source unavailable"
+ : "Choose upload source";
+ const loomImportAccessibilityLabel = loomImportError
+ ? "Loom import unavailable"
+ : sourceLoading === "loom"
+ ? loomImportTitle
+ : "Open Loom import";
+ const uploadSourceAccessibilityValue = uploadSourceError
+ ? { text: uploadSourceError }
+ : sourcePending && sourceLoadingAccessibilityText
+ ? { text: sourceLoadingAccessibilityText }
+ : sourceLoading === "loom" && sourceLoadingAccessibilityText
+ ? { text: sourceLoadingAccessibilityText }
+ : (activeUploadAccessibilityValue ?? { text: uploadAcceptedFormats });
+ const loomImportAccessibilityValue = loomImportError
+ ? { text: loomImportError }
+ : sourceLoading !== null && sourceLoadingAccessibilityText
+ ? { text: sourceLoadingAccessibilityText }
+ : activeUploadAccessibilityValue;
+ const sourceOpeningHint =
+ sourceLoading === "loom"
+ ? "Loom import is opening"
+ : "Upload source picker is opening";
+ const uploadSourceActionHint = (
+ source: Exclude,
+ idleHint: string,
+ ) => {
+ if (activeId !== null) return activeUploadHint;
+ if (sourceLoading === source) return sourceOpeningHint;
+ if (sourceLoading !== null) {
+ return sourceLoading === "loom"
+ ? "Loom import is opening"
+ : "Another upload source is opening";
+ }
+ if (sourceError?.source === source) return sourceError.message;
+ return idleHint;
+ };
+ const uploadSourceActionValue = (
+ source: Exclude,
+ ) => {
+ if (sourceLoading !== null && sourceLoadingAccessibilityText) {
+ return { text: sourceLoadingAccessibilityText };
+ }
+ if (sourceError?.source === source) return { text: sourceError.message };
+ if (activeId !== null) {
+ return activeUploadAccessibilityValue;
+ }
+ return undefined;
+ };
+ const browseFilesLabel =
+ sourceError?.source === "files" ? "Retry Files" : "Browse Files";
+ const photosLabel =
+ sourceError?.source === "photos" ? "Retry Photos" : "Photos";
+ const loomActionLabel =
+ sourceError?.source === "loom" ? "Retry Loom" : "Loom";
+ const loomActionAccessibilityLabel =
+ sourceError?.source === "loom" ? undefined : idleLoomImportLabel;
+ const uploadSourceCardBusy = activeId !== null || sourcePending;
+
+ useEffect(
+ () => () => {
+ mountedRef.current = false;
+ },
+ [],
+ );
+
+ const dispatchIfMounted = (action: Parameters[0]) => {
+ if (mountedRef.current) dispatch(action);
+ };
+
+ const setActiveUploadId = (id: string | null, fileName?: string) => {
+ activeIdRef.current = id;
+ setActiveId(id);
+ setActiveUploadName(id ? (fileName ?? null) : null);
+ };
+
+ const isUploadSourceBusy = () =>
+ sourceBusyRef.current || activeIdRef.current !== null;
+
+ const beginUploadSource = (source: UploadSource) => {
+ if (isUploadSourceBusy()) return false;
+ sourceBusyRef.current = true;
+ setSourceError(null);
+ setSourceLoading(source);
+ return true;
+ };
+
+ const endUploadSource = () => {
+ sourceBusyRef.current = false;
+ setSourceLoading(null);
+ };
+
+ const waitForProcessing = async (queueItemId: string, capId: string) => {
+ if (auth.status !== "signedIn") return;
+
+ for (const delayMs of processingPollDelaysMs) {
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
+ if (!mountedRef.current) return;
+
+ try {
+ const detail = await auth.client.getCap(capId);
+ const action = uploadQueueActionFromCapUpload(
+ queueItemId,
+ detail.cap.upload,
+ );
+ if (action) {
+ dispatchIfMounted(action);
+ if (isTerminalUploadQueueAction(action)) {
+ if (action.type === "complete") {
+ await auth.refresh().catch(() => undefined);
+ }
+ return;
+ }
+ }
+ } catch {
+ return;
+ }
+ }
+ };
+
+ const uploadQueueItem = async (
+ item: Omit | UploadQueueItem,
+ file: UploadFile,
+ ) => {
+ if (auth.status !== "signedIn") return;
+
+ setActiveUploadId(item.id, item.fileName);
+ try {
+ const created = await runMobileUpload({
+ client: auth.client,
+ file,
+ organizationId:
+ item.organizationId ?? auth.bootstrap?.activeOrganizationId,
+ folderId: item.folderId,
+ onCreated: (capId, rawFileKey) =>
+ dispatch({
+ type: "start",
+ id: item.id,
+ capId,
+ rawFileKey,
+ }),
+ onProgress: (progress) =>
+ dispatch({ type: "progress", id: item.id, progress }),
+ });
+ dispatch({ type: "processing", id: item.id, progress: 0 });
+ await auth.refresh().catch(() => undefined);
+ void waitForProcessing(item.id, created.id);
+ } catch (error) {
+ dispatch({
+ type: "fail",
+ id: item.id,
+ error: error instanceof Error ? error.message : "Upload failed",
+ });
+ } finally {
+ setActiveUploadId(null);
+ }
+ };
+
+ const uploadFile = async (file: UploadFile) => {
+ if (auth.status !== "signedIn") return;
+ setSourceError(null);
+
+ const item = queueItemFromFile(
+ file,
+ auth.bootstrap?.activeOrganizationId ?? null,
+ );
+ dispatch({ type: "enqueue", item });
+ await uploadQueueItem(item, file);
+ };
+
+ const pickFile = async () => {
+ if (!beginUploadSource("files")) return;
+ try {
+ const result = await DocumentPicker.getDocumentAsync({
+ type: "video/*",
+ copyToCacheDirectory: true,
+ });
+ if (result.canceled || !result.assets[0]) return;
+ const asset = result.assets[0];
+ endUploadSource();
+ await uploadFile({
+ uri: asset.uri,
+ name: asset.name,
+ type: contentTypeForUpload(asset.name, asset.mimeType),
+ size: asset.size,
+ });
+ } catch (error) {
+ setSourceError({
+ message: getUploadSourceErrorMessage(error, "files"),
+ source: "files",
+ });
+ } finally {
+ endUploadSource();
+ }
+ };
+
+ const pickPhoto = async () => {
+ if (!beginUploadSource("photos")) return;
+ try {
+ const permission =
+ await ImagePicker.requestMediaLibraryPermissionsAsync();
+ if (!permission.granted) {
+ setSourceError({
+ message: photosAccessNeededMessage,
+ source: "photos",
+ });
+ showPhotosSettingsAlert();
+ return;
+ }
+ const result = await ImagePicker.launchImageLibraryAsync({
+ mediaTypes: ["videos"],
+ allowsEditing: false,
+ });
+ if (result.canceled || !result.assets[0]) return;
+ const asset = result.assets[0];
+ const name = asset.fileName ?? `Cap Upload ${Date.now()}.mov`;
+ endUploadSource();
+ await uploadFile({
+ uri: asset.uri,
+ name,
+ type: contentTypeForUpload(name, asset.mimeType),
+ size: asset.fileSize,
+ durationSeconds:
+ typeof asset.duration === "number" && asset.duration > 0
+ ? asset.duration / 1000
+ : undefined,
+ width: asset.width > 0 ? asset.width : undefined,
+ height: asset.height > 0 ? asset.height : undefined,
+ });
+ } catch (error) {
+ setSourceError({
+ message: getUploadSourceErrorMessage(error, "photos"),
+ source: "photos",
+ });
+ } finally {
+ endUploadSource();
+ }
+ };
+
+ const retry = async (item: UploadQueueItem) => {
+ if (activeIdRef.current !== null) return;
+ dispatch({ type: "retry", id: item.id });
+ await uploadQueueItem(item, {
+ uri: item.localUri,
+ name: item.fileName,
+ type: item.contentType,
+ size: item.size,
+ durationSeconds: item.durationSeconds,
+ width: item.width,
+ height: item.height,
+ });
+ };
+
+ const viewCap = (capId: string | null) => {
+ if (activeIdRef.current !== null) return;
+ if (!capId) return;
+ router.push(`/caps/${capId}`);
+ };
+
+ const removeQueueItem = (item: UploadQueueItem) => {
+ if (activeIdRef.current !== null) return;
+ dispatch({ type: "remove", id: item.id });
+ };
+
+ const showQueueItemActions = (item: UploadQueueItem) => {
+ if (activeIdRef.current !== null) return;
+ const actions: Array<{
+ label: string;
+ destructive?: boolean;
+ onPress: () => void;
+ }> = [];
+
+ if (item.status === "failed") {
+ actions.push({
+ label: "Retry",
+ onPress: () => {
+ void retry(item);
+ },
+ });
+ }
+
+ if (
+ (item.status === "processing" || item.status === "complete") &&
+ item.capId
+ ) {
+ actions.push({
+ label: "View",
+ onPress: () => viewCap(item.capId),
+ });
+ }
+
+ actions.push({
+ label: "Remove from Queue",
+ destructive: true,
+ onPress: () => removeQueueItem(item),
+ });
+
+ if (Platform.OS === "ios") {
+ const cancelButtonIndex = actions.length;
+ const destructiveButtonIndex = actions.findIndex(
+ (action) => action.destructive,
+ );
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex,
+ destructiveButtonIndex:
+ destructiveButtonIndex >= 0 ? destructiveButtonIndex : undefined,
+ message: uploadQueueMetadataText(item),
+ options: [...actions.map((action) => action.label), "Cancel"],
+ title: item.fileName,
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ actions[index]?.onPress();
+ },
+ );
+ return;
+ }
+
+ Alert.alert(item.fileName, uploadQueueMetadataText(item), [
+ ...actions.map((action) => ({
+ text: action.label,
+ style: action.destructive ? ("destructive" as const) : undefined,
+ onPress: action.onPress,
+ })),
+ { text: "Cancel", style: "cancel" },
+ ]);
+ };
+
+ const showUploadSources = () => {
+ if (isUploadSourceBusy()) return;
+
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ options: ["Browse Files", "Photos", idleLoomImportLabel, "Cancel"],
+ cancelButtonIndex: 3,
+ message: uploadAcceptedFormats,
+ title: "Upload File",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) void pickFile();
+ if (index === 1) void pickPhoto();
+ if (index === 2) void openLoomImport();
+ },
+ );
+ return;
+ }
+
+ Alert.alert("Upload File", "Choose a video source.", [
+ { text: "Browse Files", onPress: () => void pickFile() },
+ { text: "Photos", onPress: () => void pickPhoto() },
+ { text: idleLoomImportLabel, onPress: () => void openLoomImport() },
+ { text: "Cancel", style: "cancel" },
+ ]);
+ };
+
+ const openLoomImport = async () => {
+ if (!beginUploadSource("loom")) return;
+
+ try {
+ const url = new URL("/dashboard/import/loom", apiBaseUrl);
+ await WebBrowser.openBrowserAsync(url.toString());
+ } catch (error) {
+ setSourceError({
+ message: getUploadSourceErrorMessage(error, "loom"),
+ source: "loom",
+ });
+ } finally {
+ endUploadSource();
+ }
+ };
+
+ if (auth.status === "loading") {
+ return ;
+ }
+
+ if (auth.status === "signedOut") {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ [
+ styles.importPressable,
+ uploadSourceBusy && styles.importPressableDisabled,
+ pressed && !uploadSourceBusy && styles.importPressablePressed,
+ ]}
+ >
+
+
+ {uploadSourceCardBusy ? (
+
+ ) : uploadSourceError ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {uploadSourceError ? "Upload source unavailable" : importTitle}
+
+
+ {importSubtitle}
+
+ {showUploadFormats ? (
+ {uploadAcceptedFormats}
+ ) : null}
+ {activeProgress !== null ? (
+
+
+
+ ) : null}
+
+
+
+
+
+
+ {
+ void openLoomImport();
+ }}
+ variant="gray"
+ loading={sourceLoading === "loom"}
+ disabled={uploadSourceBusy && sourceLoading !== "loom"}
+ style={styles.actionButton}
+ size="sm"
+ leading={}
+ />
+
+
+
+ {
+ void openLoomImport();
+ }}
+ style={({ pressed }) => [
+ styles.importPressable,
+ uploadSourceBusy && styles.importPressableDisabled,
+ pressed && !uploadSourceBusy && styles.importPressablePressed,
+ ]}
+ >
+
+
+ {loomImportError ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {loomImportTitle}
+
+ {loomImportSubtitle}
+
+
+
+
+
+
+ Queue
+ {queue.items.length === 0 ? (
+
+
+ No uploads yet
+
+ ) : (
+
+ {queue.items
+ .slice()
+ .reverse()
+ .map((item, index, items) => {
+ const isActiveQueueItem = activeId === item.id;
+ const queueActionsDisabled = activeId !== null;
+ const queueProgress = uploadProgressPercent(item.progress);
+ const showQueueProgress = uploadQueueHasProgress(item);
+ const queueStatus = uploadQueueStatusText(item);
+ const queueDisplayStatus =
+ isActiveQueueItem && item.status === "queued"
+ ? "Preparing upload"
+ : queueStatus;
+ const queueMetadata = uploadQueueMetadataText(
+ item,
+ queueDisplayStatus,
+ );
+ const queueAccessibilityValue =
+ queueActionsDisabled && activeUploadAccessibilityValue
+ ? activeUploadAccessibilityValue
+ : { text: queueMetadata };
+ const queueHint = isActiveQueueItem
+ ? item.status === "queued"
+ ? "Preparing upload"
+ : "Upload is in progress"
+ : queueActionsDisabled
+ ? "Another upload is in progress"
+ : `${queueStatus}. Opens upload actions`;
+ const queueMenuHint = queueActionsDisabled
+ ? queueHint
+ : uploadQueueMenuHint(item);
+ return (
+
+ showQueueItemActions(item)}
+ onPress={() => showQueueItemActions(item)}
+ style={({ pressed }) => [
+ styles.queueItem,
+ queueActionsDisabled &&
+ !isActiveQueueItem &&
+ styles.queueItemDisabled,
+ pressed && activeId === null && styles.queueItemPressed,
+ ]}
+ >
+
+
+ {item.fileName}
+
+ {queueMetadata}
+ {showQueueProgress ? (
+
+
+
+ ) : null}
+ {item.error ? (
+
+ {item.error}
+
+ ) : null}
+
+ {item.status === "failed" ? (
+ {
+ event?.stopPropagation();
+ void retry(item);
+ }}
+ disabled={queueActionsDisabled}
+ size="sm"
+ style={styles.viewButton}
+ symbol="arrow.clockwise"
+ variant="secondary"
+ />
+ ) : (item.status === "processing" ||
+ item.status === "complete") &&
+ item.capId ? (
+ {
+ event?.stopPropagation();
+ viewCap(item.capId);
+ }}
+ disabled={queueActionsDisabled}
+ size="sm"
+ style={styles.viewButton}
+ symbol="play.rectangle"
+ variant="secondary"
+ />
+ ) : null}
+ {
+ event.stopPropagation();
+ if (queueActionsDisabled) return;
+ showQueueItemActions(item);
+ }}
+ style={({ pressed }) => [
+ styles.queueMenuButton,
+ queueActionsDisabled &&
+ styles.queueMenuButtonDisabled,
+ pressed &&
+ !queueActionsDisabled &&
+ styles.queueMenuButtonPressed,
+ ]}
+ >
+
+
+
+ {index < items.length - 1 ? (
+
+ ) : null}
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ importCard: {
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ overflow: "hidden",
+ marginBottom: 20,
+ ...squircle,
+ },
+ importCardFallback: {
+ backgroundColor: colors.gray1,
+ },
+ importPressable: {
+ width: "100%",
+ },
+ importPressablePressed: {
+ backgroundColor: colors.gray2,
+ },
+ importPressableDisabled: {
+ opacity: 0.58,
+ },
+ importPreview: {
+ height: 128,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.gray3,
+ },
+ importIcon: {
+ width: 56,
+ height: 56,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.gray1,
+ ...squircle,
+ },
+ importBody: {
+ padding: 16,
+ },
+ importCopy: {
+ gap: 4,
+ },
+ importTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray12,
+ },
+ importSubtitle: {
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ lineHeight: 16,
+ color: colors.gray10,
+ },
+ importMeta: {
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ lineHeight: 16,
+ color: colors.gray9,
+ },
+ importErrorSubtitle: {
+ color: colors.red9,
+ },
+ importProgressTrack: {
+ height: 5,
+ borderRadius: radius.full,
+ backgroundColor: colors.gray4,
+ overflow: "hidden",
+ marginTop: 10,
+ ...squircle,
+ },
+ importProgressFill: {
+ height: "100%",
+ borderRadius: radius.full,
+ backgroundColor: colors.buttonBlue,
+ },
+ actions: {
+ flexDirection: "row",
+ gap: 10,
+ width: "100%",
+ borderTopWidth: StyleSheet.hairlineWidth,
+ borderTopColor: colors.gray3,
+ padding: 12,
+ },
+ actionButton: {
+ flex: 1,
+ },
+ queue: {
+ gap: 10,
+ },
+ sectionTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 18,
+ color: colors.gray12,
+ },
+ empty: {
+ height: 124,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ backgroundColor: colors.gray1,
+ alignItems: "center",
+ justifyContent: "center",
+ gap: 8,
+ ...squircle,
+ },
+ emptyText: {
+ fontFamily: fonts.medium,
+ color: colors.gray10,
+ },
+ queueGroup: {
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ overflow: "hidden",
+ ...squircle,
+ },
+ queueGroupFallback: {
+ backgroundColor: colors.gray1,
+ },
+ queueItem: {
+ minHeight: 78,
+ padding: 12,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 10,
+ },
+ queueItemPressed: {
+ backgroundColor: colors.gray2,
+ },
+ queueItemDisabled: {
+ opacity: 0.58,
+ },
+ queueSeparator: {
+ height: StyleSheet.hairlineWidth,
+ backgroundColor: colors.gray4,
+ marginLeft: 12,
+ },
+ queueText: {
+ flex: 1,
+ minWidth: 0,
+ gap: 3,
+ },
+ fileName: {
+ fontFamily: fonts.medium,
+ fontSize: 16,
+ color: colors.gray12,
+ },
+ fileMeta: {
+ fontFamily: fonts.regular,
+ fontSize: 13,
+ color: colors.gray10,
+ },
+ errorText: {
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ color: colors.red9,
+ },
+ viewButton: {
+ width: 88,
+ },
+ queueMenuButton: {
+ width: 42,
+ height: 42,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.gray2,
+ ...squircle,
+ },
+ queueMenuButtonPressed: {
+ backgroundColor: colors.gray4,
+ },
+ queueMenuButtonDisabled: {
+ backgroundColor: colors.gray3,
+ },
+ progressTrack: {
+ height: 4,
+ borderRadius: radius.full,
+ backgroundColor: colors.gray4,
+ overflow: "hidden",
+ marginTop: 5,
+ },
+ progressFill: {
+ height: "100%",
+ borderRadius: radius.full,
+ backgroundColor: colors.buttonBlue,
+ },
+});
diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx
new file mode 100644
index 00000000000..b5794d72fac
--- /dev/null
+++ b/apps/mobile/app/_layout.tsx
@@ -0,0 +1,110 @@
+import "react-native-gesture-handler";
+import "react-native-reanimated";
+
+import { useFonts } from "expo-font";
+import { Stack, useSegments } from "expo-router";
+import {
+ ActivityIndicator,
+ KeyboardAvoidingView,
+ Platform,
+ ScrollView,
+ StatusBar,
+ StyleSheet,
+ View,
+} from "react-native";
+import { GestureHandlerRootView } from "react-native-gesture-handler";
+import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
+import { AuthProvider, useAuth } from "@/auth/AuthContext";
+import { SignInPanel } from "@/auth/SignInPanel";
+import { signInTitleForSegments } from "@/auth/signInDestination";
+import { colors } from "@/theme";
+
+function AppShell() {
+ const auth = useAuth();
+ const segments = useSegments();
+
+ if (auth.status === "loading") {
+ return (
+
+
+
+ );
+ }
+
+ if (auth.status === "signedOut") {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ );
+}
+
+export default function RootLayout() {
+ const [fontsLoaded] = useFonts({
+ "NeueMontreal-Regular": require("../../web/public/fonts/NeueMontreal-Regular.otf"),
+ "NeueMontreal-Medium": require("../../web/public/fonts/NeueMontreal-Medium.otf"),
+ "NeueMontreal-Bold": require("../../web/public/fonts/NeueMontreal-Bold.otf"),
+ });
+
+ if (!fontsLoaded) return null;
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ loadingScreen: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.appBackground,
+ },
+ authScreen: {
+ flex: 1,
+ backgroundColor: colors.appBackground,
+ },
+ authKeyboard: {
+ flex: 1,
+ },
+ authScroll: {
+ flex: 1,
+ },
+ authContent: {
+ flexGrow: 1,
+ justifyContent: "center",
+ paddingHorizontal: 20,
+ paddingVertical: 28,
+ },
+});
diff --git a/apps/mobile/app/caps/[id].tsx b/apps/mobile/app/caps/[id].tsx
new file mode 100644
index 00000000000..4fb96c80900
--- /dev/null
+++ b/apps/mobile/app/caps/[id].tsx
@@ -0,0 +1,1105 @@
+import * as Clipboard from "expo-clipboard";
+import { router, Stack, useLocalSearchParams } from "expo-router";
+import { type SFSymbol, SymbolView } from "expo-symbols";
+import { useVideoPlayer, VideoView } from "expo-video";
+import * as WebBrowser from "expo-web-browser";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import {
+ ActionSheetIOS,
+ Alert,
+ KeyboardAvoidingView,
+ Linking,
+ Platform,
+ Pressable,
+ Share,
+ StyleSheet,
+ Text,
+ TextInput,
+ View,
+} from "react-native";
+import type { MobileCapDetail, MobilePlaybackResponse } from "@/api/mobile";
+import { apiBaseUrl, useAuth } from "@/auth/AuthContext";
+import { SignInPanel } from "@/auth/SignInPanel";
+import { CapSettingsSheet } from "@/caps/CapSettingsSheet";
+import { showCapPasswordActions } from "@/caps/passwordActions";
+import {
+ PhotosPermissionDeniedError,
+ saveCapVideoToPhotos,
+} from "@/caps/saveCapVideo";
+import { showCapTitleActions } from "@/caps/titleActions";
+import { ActionButton } from "@/components/ActionButton";
+import { GlassSurface } from "@/components/GlassSurface";
+import { Screen } from "@/components/Screen";
+import { colors, fonts, radius, squircle } from "@/theme";
+import { formatRelativeDate } from "@/utils/format";
+
+const showPhotosSettingsAlert = () => {
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: 1,
+ message: "Allow Cap to save videos to Photos from Settings.",
+ options: ["Open Settings", "Cancel"],
+ title: "Photos access needed",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) void Linking.openSettings();
+ },
+ );
+ return;
+ }
+
+ Alert.alert(
+ "Photos access needed",
+ "Allow Cap to save videos to Photos from Settings.",
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Open Settings",
+ onPress: () => {
+ void Linking.openSettings();
+ },
+ },
+ ],
+ );
+};
+
+const getCapDetailErrorMessage = (error: unknown) =>
+ error instanceof Error ? error.message : "Unable to load this Cap";
+
+type CapDetailOperation = "comment" | "save" | "visibility";
+
+type AnalyticsMetricProps = {
+ symbol: SFSymbol;
+ value: number;
+};
+
+function AnalyticsMetric({ symbol, value }: AnalyticsMetricProps) {
+ return (
+
+
+ {value}
+
+ );
+}
+
+export default function CapDetailScreen() {
+ const { id } = useLocalSearchParams<{ id: string }>();
+ const auth = useAuth();
+ const [detail, setDetail] = useState(null);
+ const [playback, setPlayback] = useState(null);
+ const [comment, setComment] = useState("");
+ const [loading, setLoading] = useState(true);
+ const [activeOperation, setActiveOperation] =
+ useState(null);
+ const [loadError, setLoadError] = useState(null);
+ const [copied, setCopied] = useState(false);
+ const [saved, setSaved] = useState(false);
+ const [settingsVisible, setSettingsVisible] = useState(false);
+ const player = useVideoPlayer(null);
+
+ const load = useCallback(async () => {
+ if (auth.status !== "signedIn" || typeof id !== "string") return;
+ setLoading(true);
+ setLoadError(null);
+ try {
+ const [nextDetail, nextPlayback] = await Promise.all([
+ auth.client.getCap(id),
+ auth.client.getPlayback(id),
+ ]);
+ setDetail(nextDetail);
+ setPlayback(nextPlayback);
+ } catch (error) {
+ setDetail(null);
+ setPlayback(null);
+ setLoadError(getCapDetailErrorMessage(error));
+ } finally {
+ setLoading(false);
+ }
+ }, [auth, id]);
+
+ useEffect(() => {
+ load().catch(() => {});
+ }, [load]);
+
+ useEffect(() => {
+ if (!playback?.url) return;
+ player.replace(playback.url);
+ }, [playback?.url, player]);
+
+ useEffect(() => {
+ if (!copied) return;
+ const timeout = setTimeout(() => setCopied(false), 1600);
+ return () => clearTimeout(timeout);
+ }, [copied]);
+
+ useEffect(() => {
+ if (!saved) return;
+ const timeout = setTimeout(() => setSaved(false), 1600);
+ return () => clearTimeout(timeout);
+ }, [saved]);
+
+ const textComments = useMemo(
+ () => detail?.comments.filter((item) => item.type === "text") ?? [],
+ [detail],
+ );
+ const reactions = useMemo(
+ () => detail?.comments.filter((item) => item.type === "emoji") ?? [],
+ [detail],
+ );
+ const isActionInProgress = activeOperation !== null;
+ const isPostingComment = activeOperation === "comment";
+ const isSavingVideo = activeOperation === "save";
+ const isUpdatingVisibility = activeOperation === "visibility";
+ const actionInProgressHint = "Current Cap action is in progress";
+ const saveVideoLabel = saved ? "Saved" : "Save video";
+ const saveVideoAccessibilityText =
+ isSavingVideo && detail
+ ? `Saving video for ${detail.cap.title}`
+ : saved && detail
+ ? `Saved video for ${detail.cap.title}`
+ : undefined;
+ const saveVideoAccessibilityLabel = saved
+ ? saveVideoAccessibilityText
+ : undefined;
+ const saveVideoAccessibilityValue =
+ isSavingVideo && saveVideoAccessibilityText
+ ? { text: saveVideoAccessibilityText }
+ : undefined;
+ const saveVideoHint = isSavingVideo
+ ? "Save is in progress"
+ : isActionInProgress
+ ? actionInProgressHint
+ : "Saves this video to Photos";
+ const sharingStatusHint = isUpdatingVisibility
+ ? "Sharing update is in progress"
+ : isActionInProgress
+ ? actionInProgressHint
+ : "Opens sharing settings";
+ const sharingStatusLabel = detail?.cap.public ? "Shared" : "Not shared";
+ const sharingStatusAccessibilityValue =
+ isUpdatingVisibility && detail
+ ? `Updating sharing for ${detail.cap.title}`
+ : undefined;
+ const commentHint = isPostingComment
+ ? "Comment is being sent"
+ : isActionInProgress
+ ? actionInProgressHint
+ : "Add a comment to this Cap";
+ const sendCommentHint = isPostingComment
+ ? "Comment is being sent"
+ : isActionInProgress
+ ? actionInProgressHint
+ : comment.trim().length > 0
+ ? "Adds this comment"
+ : "Enter a comment before sending";
+ const sendCommentLabel = isPostingComment ? "Sending..." : "Send";
+ const sendCommentAccessibilityLabel =
+ isPostingComment && detail
+ ? `Sending comment on ${detail.cap.title}`
+ : "Send comment";
+ const canSendComment = comment.trim().length > 0 && !isActionInProgress;
+
+ const createComment = async () => {
+ const trimmed = comment.trim();
+ if (!trimmed || !detail || isActionInProgress) return;
+ setActiveOperation("comment");
+ try {
+ const created = await auth.client.createComment(detail.cap.id, {
+ content: trimmed,
+ timestamp: null,
+ });
+ setDetail({
+ ...detail,
+ comments: [...detail.comments, created],
+ cap: {
+ ...detail.cap,
+ commentCount: detail.cap.commentCount + 1,
+ },
+ });
+ setComment("");
+ } catch (error) {
+ Alert.alert(
+ "Comment failed",
+ error instanceof Error ? error.message : "Unable to add that comment.",
+ );
+ } finally {
+ setActiveOperation(null);
+ }
+ };
+
+ const createReaction = async (emoji: string) => {
+ if (!detail) return;
+ try {
+ const created = await auth.client.createReaction(detail.cap.id, {
+ content: emoji,
+ timestamp: null,
+ });
+ setDetail({
+ ...detail,
+ comments: [...detail.comments, created],
+ cap: {
+ ...detail.cap,
+ reactionCount: detail.cap.reactionCount + 1,
+ },
+ });
+ } catch (error) {
+ Alert.alert(
+ "Reaction failed",
+ error instanceof Error ? error.message : "Unable to add that reaction.",
+ );
+ }
+ };
+
+ const copyLink = async () => {
+ if (!detail) return;
+ try {
+ await Clipboard.setStringAsync(detail.shareUrl);
+ setCopied(true);
+ } catch (error) {
+ Alert.alert(
+ "Copy failed",
+ error instanceof Error ? error.message : "Unable to copy this link.",
+ );
+ }
+ };
+
+ const shareLink = async () => {
+ if (!detail) return;
+ await Share.share({ url: detail.shareUrl, message: detail.shareUrl });
+ };
+
+ const updateVisibility = async (isPublic: boolean) => {
+ if (!detail || isActionInProgress) return;
+ setActiveOperation("visibility");
+ try {
+ const cap = await auth.client.updateCapSharing(detail.cap.id, {
+ public: isPublic,
+ });
+ setDetail((current) => (current ? { ...current, cap } : current));
+ await auth.refresh();
+ } catch (error) {
+ Alert.alert(
+ "Sharing update failed",
+ error instanceof Error
+ ? error.message
+ : "Unable to update sharing for this Cap.",
+ );
+ } finally {
+ setActiveOperation(null);
+ }
+ };
+
+ const showPasswordActions = () => {
+ if (!detail || auth.status !== "signedIn") return;
+ showCapPasswordActions({
+ cap: detail.cap,
+ client: auth.client,
+ onUpdated: async (cap) => {
+ setDetail((current) => (current ? { ...current, cap } : current));
+ await auth.refresh();
+ },
+ });
+ };
+
+ const showTitleActions = () => {
+ if (!detail || auth.status !== "signedIn") return;
+ showCapTitleActions({
+ cap: detail.cap,
+ client: auth.client,
+ onUpdated: async (cap) => {
+ setDetail((current) => (current ? { ...current, cap } : current));
+ await auth.refresh();
+ },
+ });
+ };
+
+ const downloadVideo = async () => {
+ if (!detail || isActionInProgress) return;
+ setActiveOperation("save");
+ try {
+ await saveCapVideoToPhotos(auth.client, detail.cap.id);
+ setSaved(true);
+ } catch (error) {
+ if (error instanceof PhotosPermissionDeniedError) {
+ showPhotosSettingsAlert();
+ return;
+ }
+ Alert.alert(
+ "Save failed",
+ error instanceof Error ? error.message : "Unable to save this video.",
+ );
+ } finally {
+ setActiveOperation(null);
+ }
+ };
+
+ const deleteCap = () => {
+ if (!detail || isActionInProgress) return;
+ const confirmDelete = () => {
+ void (async () => {
+ setSettingsVisible(false);
+ await auth.client.deleteCap(detail.cap.id);
+ await auth.refresh();
+ router.back();
+ })();
+ };
+
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: 1,
+ destructiveButtonIndex: 0,
+ message: "This Cap will be removed from your library.",
+ options: ["Delete Cap", "Cancel"],
+ title: "Delete Cap",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) confirmDelete();
+ },
+ );
+ return;
+ }
+
+ Alert.alert("Delete Cap", "This Cap will be removed from your library.", [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Delete",
+ style: "destructive",
+ onPress: confirmDelete,
+ },
+ ]);
+ };
+
+ const showMoreActions = () => {
+ setSettingsVisible(true);
+ };
+
+ const viewAnalytics = () => {
+ if (!detail || isActionInProgress) return;
+ const url = new URL("/dashboard/analytics", apiBaseUrl);
+ url.searchParams.set("capId", detail.cap.id);
+ void WebBrowser.openBrowserAsync(url.toString());
+ };
+
+ if (auth.status === "signedOut") {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ detail ? (
+ [
+ styles.headerAction,
+ pressed && !isActionInProgress
+ ? styles.headerActionPressed
+ : null,
+ isActionInProgress ? styles.headerActionDisabled : null,
+ ]}
+ >
+
+
+ ) : null,
+ title: detail?.cap.title ?? "Cap",
+ }}
+ />
+
+ {loadError ? (
+
+
+ Unable to load Cap
+ {loadError}
+ {
+ void load();
+ }}
+ symbol="arrow.clockwise"
+ style={styles.retryButton}
+ />
+
+ ) : detail ? (
+ <>
+
+ {playback?.url ? (
+
+ ) : (
+
+ Processing video
+
+ )}
+
+
+ {detail.cap.title}
+
+ {formatRelativeDate(detail.cap.createdAt)} ยท{" "}
+ {detail.cap.ownerName}
+
+
+ setSettingsVisible(true)}
+ style={({ pressed }) => [
+ styles.shareStatusButton,
+ pressed && !isActionInProgress
+ ? styles.shareStatusButtonPressed
+ : null,
+ isActionInProgress
+ ? styles.shareStatusButtonDisabled
+ : null,
+ ]}
+ >
+
+
+ {sharingStatusLabel}
+
+
+
+ {detail.cap.protected ? (
+
+
+
+ Password protected
+
+
+ ) : null}
+
+
+
+
+
+
+
+ [
+ styles.analyticsPanel,
+ pressed && styles.analyticsPanelPressed,
+ ]}
+ >
+
+
+
+
+
+ View analytics
+
+ {detail.summary ? (
+
+ Summary
+ {detail.summary}
+
+ ) : null}
+ {detail.chapters.length > 0 ? (
+
+ Chapters
+ {detail.chapters.map((chapter) => (
+
+
+ {Math.floor(chapter.start / 60)}:
+ {Math.floor(chapter.start % 60)
+ .toString()
+ .padStart(2, "0")}
+
+
+ {chapter.title}
+
+
+ ))}
+
+ ) : null}
+
+
+ Reactions
+ {reactions.length}
+
+
+ {["๐", "๐", "๐ฅ", "๐"].map((emoji) => (
+ createReaction(emoji)}
+ style={styles.reactionButton}
+ >
+ {emoji}
+
+ ))}
+
+
+
+
+ Comments
+ {textComments.length}
+
+
+ {
+ void createComment();
+ }}
+ placeholder="Add a comment"
+ placeholderTextColor={colors.gray9}
+ returnKeyType="send"
+ selectionColor={colors.blue11}
+ style={[
+ styles.commentInput,
+ isActionInProgress ? styles.commentInputDisabled : null,
+ ]}
+ submitBehavior="blurAndSubmit"
+ value={comment}
+ multiline
+ />
+
+
+ {textComments.map((item) => (
+
+
+
+
+
+
+ {item.author.name ?? "Cap user"}
+
+ {item.content}
+
+
+ ))}
+
+ >
+ ) : null}
+
+ setSettingsVisible(false)}
+ onCopyLink={() => {
+ void copyLink();
+ }}
+ onDelete={() => deleteCap()}
+ onPassword={() => showPasswordActions()}
+ onRename={() => showTitleActions()}
+ onSaveVideo={() => {
+ void downloadVideo();
+ }}
+ onShareLink={() => {
+ void shareLink();
+ }}
+ onViewAnalytics={() => viewAnalytics()}
+ onVisibilityChange={(_cap, isPublic) => {
+ void updateVisibility(isPublic);
+ }}
+ saveDisabled={isActionInProgress}
+ saveDisabledHint={saveVideoHint}
+ saveDisabledValue={isSavingVideo ? undefined : "Unavailable"}
+ saveDisabledAccessibilityValue={
+ isSavingVideo ? saveVideoAccessibilityText : undefined
+ }
+ visibilityDisabled={isActionInProgress}
+ visibilityDisabledHint={sharingStatusHint}
+ visibilityDisabledAccessibilityValue={sharingStatusAccessibilityValue}
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ keyboard: {
+ flex: 1,
+ },
+ headerAction: {
+ width: 36,
+ height: 36,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ headerActionPressed: {
+ backgroundColor: colors.gray3,
+ },
+ headerActionDisabled: {
+ opacity: 0.55,
+ },
+ videoFrame: {
+ width: "100%",
+ aspectRatio: 16 / 9,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ overflow: "hidden",
+ backgroundColor: colors.black,
+ marginBottom: 14,
+ ...squircle,
+ },
+ video: {
+ width: "100%",
+ height: "100%",
+ },
+ videoPlaceholder: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ placeholderText: {
+ fontFamily: fonts.medium,
+ color: colors.gray10,
+ },
+ titleBlock: {
+ gap: 4,
+ marginBottom: 14,
+ },
+ title: {
+ fontFamily: fonts.medium,
+ fontSize: 24,
+ lineHeight: 30,
+ color: colors.gray12,
+ },
+ meta: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray10,
+ },
+ statusRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ flexWrap: "wrap",
+ gap: 8,
+ marginTop: 4,
+ },
+ shareStatusButton: {
+ minHeight: 30,
+ maxWidth: "100%",
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 6,
+ borderRadius: radius.full,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray4,
+ backgroundColor: colors.gray1,
+ paddingHorizontal: 11,
+ ...squircle,
+ },
+ shareStatusButtonPressed: {
+ backgroundColor: colors.gray3,
+ borderColor: colors.gray5,
+ },
+ shareStatusButtonDisabled: {
+ backgroundColor: colors.gray2,
+ borderColor: colors.gray3,
+ },
+ shareStatusText: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 19,
+ color: colors.gray10,
+ },
+ shareStatusTextDisabled: {
+ color: colors.gray9,
+ },
+ passwordPill: {
+ minHeight: 30,
+ maxWidth: "100%",
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 6,
+ borderRadius: radius.full,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray4,
+ backgroundColor: colors.gray1,
+ paddingHorizontal: 11,
+ ...squircle,
+ },
+ passwordPillText: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 19,
+ color: colors.gray10,
+ },
+ actions: {
+ flexDirection: "row",
+ flexWrap: "wrap",
+ gap: 8,
+ marginBottom: 18,
+ },
+ actionButton: {
+ flexBasis: 112,
+ flexGrow: 1,
+ },
+ analyticsPanel: {
+ minHeight: 42,
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ gap: 12,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ backgroundColor: colors.gray1,
+ paddingHorizontal: 14,
+ paddingVertical: 10,
+ marginBottom: 20,
+ ...squircle,
+ },
+ analyticsPanelPressed: {
+ backgroundColor: colors.gray2,
+ borderColor: colors.blue10,
+ },
+ analyticsMetrics: {
+ flexDirection: "row",
+ alignItems: "center",
+ flexWrap: "wrap",
+ gap: 16,
+ flexShrink: 1,
+ },
+ metric: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 7,
+ },
+ metricText: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ color: colors.gray12,
+ },
+ analyticsLink: {
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ lineHeight: 17,
+ color: colors.blue11,
+ },
+ errorCard: {
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ backgroundColor: colors.gray1,
+ alignItems: "center",
+ gap: 10,
+ paddingHorizontal: 18,
+ paddingVertical: 24,
+ ...squircle,
+ },
+ errorTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 19,
+ color: colors.gray12,
+ textAlign: "center",
+ },
+ errorBody: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray10,
+ textAlign: "center",
+ },
+ retryButton: {
+ marginTop: 4,
+ minWidth: 150,
+ },
+ section: {
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ gap: 10,
+ padding: 16,
+ marginBottom: 20,
+ ...squircle,
+ },
+ sectionFallback: {
+ backgroundColor: colors.gray1,
+ },
+ sectionHeader: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ },
+ sectionTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 18,
+ lineHeight: 23,
+ color: colors.gray12,
+ },
+ countText: {
+ fontFamily: fonts.medium,
+ fontSize: 14,
+ color: colors.gray10,
+ },
+ bodyText: {
+ fontFamily: fonts.regular,
+ fontSize: 15,
+ lineHeight: 23,
+ color: colors.gray11,
+ },
+ chapter: {
+ minHeight: 48,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 10,
+ borderRadius: radius.sm,
+ backgroundColor: colors.gray2,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ padding: 10,
+ ...squircle,
+ },
+ chapterTime: {
+ width: 44,
+ fontFamily: fonts.medium,
+ fontSize: 13,
+ color: colors.blue11,
+ },
+ chapterTitle: {
+ flex: 1,
+ fontFamily: fonts.medium,
+ fontSize: 14,
+ color: colors.gray12,
+ },
+ reactions: {
+ flexDirection: "row",
+ gap: 8,
+ },
+ reactionButton: {
+ width: 52,
+ },
+ commentInputRow: {
+ flexDirection: "row",
+ alignItems: "flex-end",
+ gap: 8,
+ },
+ commentInput: {
+ flex: 1,
+ minHeight: 46,
+ maxHeight: 120,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray4,
+ backgroundColor: colors.gray2,
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ fontFamily: fonts.regular,
+ fontSize: 15,
+ color: colors.gray12,
+ ...squircle,
+ },
+ commentInputDisabled: {
+ backgroundColor: colors.gray3,
+ color: colors.gray10,
+ },
+ sendButton: {
+ width: 92,
+ },
+ comment: {
+ flexDirection: "row",
+ gap: 10,
+ backgroundColor: colors.gray2,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ padding: 12,
+ ...squircle,
+ },
+ commentIcon: {
+ width: 30,
+ height: 30,
+ borderRadius: radius.sm,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.blue3,
+ ...squircle,
+ },
+ commentBody: {
+ flex: 1,
+ minWidth: 0,
+ gap: 3,
+ },
+ commentAuthor: {
+ fontFamily: fonts.medium,
+ fontSize: 14,
+ color: colors.gray12,
+ },
+ commentText: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray11,
+ },
+});
diff --git a/apps/mobile/assets/icon.png b/apps/mobile/assets/icon.png
new file mode 100644
index 00000000000..b81ddeeef92
Binary files /dev/null and b/apps/mobile/assets/icon.png differ
diff --git a/apps/mobile/assets/icon.svg b/apps/mobile/assets/icon.svg
new file mode 100644
index 00000000000..5e6e3521098
--- /dev/null
+++ b/apps/mobile/assets/icon.svg
@@ -0,0 +1,6 @@
+
diff --git a/apps/mobile/assets/splash-icon.png b/apps/mobile/assets/splash-icon.png
new file mode 100644
index 00000000000..b480ea2b880
Binary files /dev/null and b/apps/mobile/assets/splash-icon.png differ
diff --git a/apps/mobile/assets/splash-icon.svg b/apps/mobile/assets/splash-icon.svg
new file mode 100644
index 00000000000..2a3ccf24753
--- /dev/null
+++ b/apps/mobile/assets/splash-icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/apps/mobile/babel.config.js b/apps/mobile/babel.config.js
new file mode 100644
index 00000000000..8f92bed32c1
--- /dev/null
+++ b/apps/mobile/babel.config.js
@@ -0,0 +1,7 @@
+module.exports = (api) => {
+ api.cache(true);
+ return {
+ presets: ["babel-preset-expo"],
+ plugins: ["react-native-reanimated/plugin"],
+ };
+};
diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js
new file mode 100644
index 00000000000..9c8cf0a0e8d
--- /dev/null
+++ b/apps/mobile/metro.config.js
@@ -0,0 +1,14 @@
+const path = require("node:path");
+const { getDefaultConfig } = require("expo/metro-config");
+
+const projectRoot = __dirname;
+const workspaceRoot = path.resolve(projectRoot, "../..");
+const config = getDefaultConfig(projectRoot);
+
+config.watchFolders = [workspaceRoot];
+config.resolver.nodeModulesPaths = [
+ path.resolve(projectRoot, "node_modules"),
+ path.resolve(workspaceRoot, "node_modules"),
+];
+
+module.exports = config;
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
new file mode 100644
index 00000000000..868773eb458
--- /dev/null
+++ b/apps/mobile/package.json
@@ -0,0 +1,60 @@
+{
+ "name": "@cap/mobile",
+ "version": "0.1.0",
+ "private": true,
+ "main": "expo-router/entry",
+ "scripts": {
+ "dev": "CAP_MOBILE_DISABLE_ASSOCIATED_DOMAINS=1 node scripts/run-ios-simulator.mjs",
+ "dev:device": "expo run:ios --device",
+ "start": "expo start --dev-client",
+ "typecheck": "tsc --noEmit",
+ "test": "vitest run",
+ "prebuild:ios": "expo prebuild --platform ios --no-install",
+ "ios": "expo run:ios"
+ },
+ "dependencies": {
+ "@cap/web-domain": "workspace:*",
+ "@expo/config-plugins": "~55.0.9",
+ "@expo/metro-runtime": "~55.0.11",
+ "@shopify/flash-list": "2.0.2",
+ "effect": "^3.18.4",
+ "expo": "~55.0.24",
+ "expo-clipboard": "~55.0.13",
+ "expo-constants": "~55.0.16",
+ "expo-dev-client": "~55.0.34",
+ "expo-document-picker": "~55.0.13",
+ "expo-file-system": "~55.0.20",
+ "expo-font": "~55.0.7",
+ "expo-glass-effect": "~55.0.11",
+ "expo-image": "~55.0.10",
+ "expo-image-picker": "~55.0.20",
+ "expo-linking": "~55.0.15",
+ "expo-media-library": "~55.0.17",
+ "expo-modules-core": "~55.0.25",
+ "expo-router": "~55.0.14",
+ "expo-secure-store": "~55.0.14",
+ "expo-sharing": "~55.0.19",
+ "expo-symbols": "~55.0.8",
+ "expo-video": "~55.0.17",
+ "expo-web-browser": "~55.0.16",
+ "react": "19.2.0",
+ "react-dom": "19.2.0",
+ "react-native": "0.83.6",
+ "react-native-gesture-handler": "~2.30.0",
+ "react-native-reanimated": "4.2.1",
+ "react-native-safe-area-context": "~5.6.2",
+ "react-native-screens": "~4.23.0",
+ "react-native-svg": "15.15.5",
+ "react-native-web": "~0.21.0",
+ "react-native-worklets": "0.7.4"
+ },
+ "devDependencies": {
+ "@testing-library/react-native": "^13.3.3",
+ "@types/react": "19.2.14",
+ "@types/react-test-renderer": "^19.1.0",
+ "babel-preset-expo": "~55.0.21",
+ "react-test-renderer": "19.2.0",
+ "typescript": "~5.9.2",
+ "vitest": "^3.2.0"
+ }
+}
diff --git a/apps/mobile/scripts/run-ios-simulator.mjs b/apps/mobile/scripts/run-ios-simulator.mjs
new file mode 100644
index 00000000000..1ea40fa0d75
--- /dev/null
+++ b/apps/mobile/scripts/run-ios-simulator.mjs
@@ -0,0 +1,107 @@
+import { spawnSync } from "node:child_process";
+import { existsSync, readFileSync } from "node:fs";
+import { join } from "node:path";
+
+const readSimulators = () => {
+ const result = spawnSync(
+ "xcrun",
+ ["simctl", "list", "devices", "available", "--json"],
+ {
+ encoding: "utf8",
+ },
+ );
+ if (result.status !== 0) {
+ throw new Error(result.stderr || "Unable to list iOS simulators");
+ }
+
+ return JSON.parse(result.stdout);
+};
+
+const findSimulator = () => {
+ const requestedUdid = process.env.IOS_SIMULATOR_UDID;
+ const requestedName = process.env.IOS_SIMULATOR_DEVICE;
+ const data = readSimulators();
+ const devices = Object.values(data.devices ?? {})
+ .flat()
+ .filter(
+ (device) => device?.isAvailable && device?.name?.includes("iPhone"),
+ );
+
+ if (requestedUdid) {
+ const requested = devices.find((device) => device.udid === requestedUdid);
+ if (requested) return requested;
+ throw new Error(`No available iPhone simulator found for ${requestedUdid}`);
+ }
+
+ if (requestedName) {
+ const requested = devices.find((device) => device.name === requestedName);
+ if (requested) return requested;
+ throw new Error(`No available iPhone simulator named ${requestedName}`);
+ }
+
+ const booted = devices.find((device) => device.state === "Booted");
+ if (booted) return booted;
+
+ const preferred = devices.find((device) => device.name.includes("Pro"));
+ return preferred ?? devices[0] ?? null;
+};
+
+const simulator = findSimulator();
+if (!simulator) {
+ throw new Error("No available iPhone simulators found");
+}
+
+const needsDevPrebuild = () => {
+ if (existsSync(join(process.cwd(), "ios", "CapBroadcastExtension"))) {
+ return true;
+ }
+ const entitlementsPath = join(
+ process.cwd(),
+ "ios",
+ "Cap",
+ "Cap.entitlements",
+ );
+ if (!existsSync(entitlementsPath)) return true;
+ const entitlements = readFileSync(entitlementsPath, "utf8");
+ return entitlements.includes("com.apple.developer.associated-domains");
+};
+
+const command = ["exec", "expo", "run:ios", "--device", simulator.udid];
+console.log(`Using iOS simulator: ${simulator.name} (${simulator.udid})`);
+
+if (process.env.CAP_MOBILE_DRY_RUN === "1") {
+ console.log(`pnpm ${command.join(" ")}`);
+ process.exit(0);
+}
+
+if (
+ process.env.CAP_MOBILE_DISABLE_ASSOCIATED_DOMAINS === "1" &&
+ needsDevPrebuild()
+) {
+ const prebuild = spawnSync(
+ "pnpm",
+ [
+ "exec",
+ "expo",
+ "prebuild",
+ "--platform",
+ "ios",
+ "--no-install",
+ "--clean",
+ ],
+ {
+ stdio: "inherit",
+ env: process.env,
+ },
+ );
+ if (prebuild.status !== 0) {
+ process.exit(prebuild.status ?? 1);
+ }
+}
+
+const result = spawnSync("pnpm", command, {
+ stdio: "inherit",
+ env: process.env,
+});
+
+process.exit(result.status ?? 1);
diff --git a/apps/mobile/src/api/mobile.test.ts b/apps/mobile/src/api/mobile.test.ts
new file mode 100644
index 00000000000..a4d1407a0fc
--- /dev/null
+++ b/apps/mobile/src/api/mobile.test.ts
@@ -0,0 +1,447 @@
+import { describe, expect, it, vi } from "vitest";
+import {
+ createMobileApiClient,
+ createSessionRequestUrl,
+ uploadToTarget,
+} from "./mobile";
+
+const fileSystemMock = vi.hoisted(() => ({
+ FileSystemUploadType: {
+ BINARY_CONTENT: 0,
+ MULTIPART: 1,
+ },
+ createUploadTask: vi.fn(),
+ getInfoAsync: vi.fn(),
+}));
+
+vi.mock("expo-file-system/legacy", () => fileSystemMock);
+
+describe("createMobileApiClient", () => {
+ it("decodes bootstrap responses through shared schemas", async () => {
+ const calls: RequestInfo[] = [];
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = (async (input: RequestInfo | URL) => {
+ calls.push(input as RequestInfo);
+ return new Response(
+ JSON.stringify({
+ user: {
+ id: "user_123",
+ name: "Richie",
+ email: "richie@example.com",
+ imageUrl: null,
+ activeOrganizationId: "org_123",
+ },
+ organizations: [
+ {
+ id: "org_123",
+ name: "Cap",
+ iconUrl: null,
+ role: "owner",
+ },
+ ],
+ activeOrganizationId: "org_123",
+ rootFolders: [],
+ }),
+ { status: 200 },
+ );
+ }) as typeof fetch;
+
+ try {
+ const client = createMobileApiClient({
+ baseUrl: "https://cap.so/",
+ getToken: () => "api-key",
+ });
+ const result = await client.bootstrap();
+ expect(result.user.email).toBe("richie@example.com");
+ expect(String(calls[0])).toBe("https://cap.so/api/mobile/bootstrap");
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+ });
+
+ it("decodes public auth provider config", async () => {
+ const calls: RequestInfo[] = [];
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = (async (input: RequestInfo | URL) => {
+ calls.push(input as RequestInfo);
+ return new Response(
+ JSON.stringify({
+ googleAuthAvailable: false,
+ workosAuthAvailable: true,
+ }),
+ { status: 200 },
+ );
+ }) as typeof fetch;
+
+ try {
+ const client = createMobileApiClient({
+ baseUrl: "https://cap.so/",
+ getToken: () => null,
+ });
+ const result = await client.getAuthConfig();
+ expect(result.googleAuthAvailable).toBe(false);
+ expect(result.workosAuthAvailable).toBe(true);
+ expect(String(calls[0])).toBe("https://cap.so/api/mobile/session/config");
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+ });
+
+ it("builds Google session request URLs", () => {
+ expect(
+ createSessionRequestUrl("https://cap.so/", "cap://auth", "google"),
+ ).toBe(
+ "https://cap.so/api/mobile/session/request?redirectUri=cap%3A%2F%2Fauth&provider=google",
+ );
+ });
+
+ it("builds WorkOS session request URLs", () => {
+ expect(
+ createSessionRequestUrl(
+ "https://cap.so/",
+ "cap://auth",
+ "workos",
+ "org_123",
+ ),
+ ).toBe(
+ "https://cap.so/api/mobile/session/request?redirectUri=cap%3A%2F%2Fauth&provider=workos&organizationId=org_123",
+ );
+ });
+
+ it("updates Cap sharing with the authenticated PATCH endpoint", async () => {
+ const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = (async (
+ input: RequestInfo | URL,
+ init?: RequestInit,
+ ) => {
+ calls.push({ input, init });
+ return new Response(
+ JSON.stringify({
+ id: "video_123",
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: null,
+ thumbnailUrl: null,
+ folderId: null,
+ public: false,
+ protected: false,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: null,
+ }),
+ { status: 200 },
+ );
+ }) as typeof fetch;
+
+ try {
+ const client = createMobileApiClient({
+ baseUrl: "https://cap.so/",
+ getToken: () => "api-key",
+ });
+ const result = await client.updateCapSharing("video_123", {
+ public: false,
+ });
+ const body = calls[0]?.init?.body;
+
+ expect(result.public).toBe(false);
+ expect(String(calls[0]?.input)).toBe(
+ "https://cap.so/api/mobile/caps/video_123/sharing",
+ );
+ expect(calls[0]?.init?.method).toBe("PATCH");
+ expect(calls[0]?.init?.headers).toBeInstanceOf(Headers);
+ expect((calls[0]?.init?.headers as Headers).get("authorization")).toBe(
+ "Bearer api-key",
+ );
+ expect(typeof body).toBe("string");
+ expect(JSON.parse(body as string)).toEqual({ public: false });
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+ });
+
+ it("creates folders with the authenticated POST endpoint", async () => {
+ const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = (async (
+ input: RequestInfo | URL,
+ init?: RequestInit,
+ ) => {
+ calls.push({ input, init });
+ return new Response(
+ JSON.stringify({
+ id: "folder_123",
+ name: "Product",
+ color: "blue",
+ parentId: null,
+ videoCount: 0,
+ }),
+ { status: 200 },
+ );
+ }) as typeof fetch;
+
+ try {
+ const client = createMobileApiClient({
+ baseUrl: "https://cap.so/",
+ getToken: () => "api-key",
+ });
+ const result = await client.createFolder({
+ name: "Product",
+ color: "blue",
+ });
+ const body = calls[0]?.init?.body;
+
+ expect(result.name).toBe("Product");
+ expect(String(calls[0]?.input)).toBe("https://cap.so/api/mobile/folders");
+ expect(calls[0]?.init?.method).toBe("POST");
+ expect(typeof body).toBe("string");
+ expect(JSON.parse(body as string)).toEqual({
+ name: "Product",
+ color: "blue",
+ });
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+ });
+
+ it("updates Cap titles with the authenticated PATCH endpoint", async () => {
+ const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = (async (
+ input: RequestInfo | URL,
+ init?: RequestInit,
+ ) => {
+ calls.push({ input, init });
+ return new Response(
+ JSON.stringify({
+ id: "video_123",
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Roadmap review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: null,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: false,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: null,
+ }),
+ { status: 200 },
+ );
+ }) as typeof fetch;
+
+ try {
+ const client = createMobileApiClient({
+ baseUrl: "https://cap.so/",
+ getToken: () => "api-key",
+ });
+ const result = await client.updateCapTitle("video_123", {
+ title: "Roadmap review",
+ });
+ const body = calls[0]?.init?.body;
+
+ expect(result.title).toBe("Roadmap review");
+ expect(String(calls[0]?.input)).toBe(
+ "https://cap.so/api/mobile/caps/video_123/title",
+ );
+ expect(calls[0]?.init?.method).toBe("PATCH");
+ expect(typeof body).toBe("string");
+ expect(JSON.parse(body as string)).toEqual({ title: "Roadmap review" });
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+ });
+
+ it("updates Cap passwords with the authenticated PATCH endpoint", async () => {
+ const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
+ const originalFetch = globalThis.fetch;
+ globalThis.fetch = (async (
+ input: RequestInfo | URL,
+ init?: RequestInit,
+ ) => {
+ calls.push({ input, init });
+ return new Response(
+ JSON.stringify({
+ id: "video_123",
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: null,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: true,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: null,
+ }),
+ { status: 200 },
+ );
+ }) as typeof fetch;
+
+ try {
+ const client = createMobileApiClient({
+ baseUrl: "https://cap.so/",
+ getToken: () => "api-key",
+ });
+ const result = await client.updateCapPassword("video_123", {
+ password: "secret",
+ });
+ const body = calls[0]?.init?.body;
+
+ expect(result.protected).toBe(true);
+ expect(String(calls[0]?.input)).toBe(
+ "https://cap.so/api/mobile/caps/video_123/password",
+ );
+ expect(calls[0]?.init?.method).toBe("PATCH");
+ expect(typeof body).toBe("string");
+ expect(JSON.parse(body as string)).toEqual({ password: "secret" });
+ } finally {
+ globalThis.fetch = originalFetch;
+ }
+ });
+
+ it("uploads local files with native transfer progress", async () => {
+ const uploadAsync = vi.fn(() =>
+ Promise.resolve({
+ body: "",
+ headers: {},
+ mimeType: null,
+ status: 200,
+ }),
+ );
+ const onProgress = vi.fn();
+
+ fileSystemMock.createUploadTask.mockImplementation(
+ (
+ url: string,
+ fileUri: string,
+ options: unknown,
+ callback?: (data: {
+ totalBytesExpectedToSend: number;
+ totalBytesSent: number;
+ }) => void,
+ ) => {
+ callback?.({
+ totalBytesExpectedToSend: 3,
+ totalBytesSent: 2,
+ });
+ return { uploadAsync, url, fileUri, options };
+ },
+ );
+
+ await uploadToTarget(
+ {
+ type: "driveResumable",
+ url: "https://uploads.example/drive",
+ headers: {
+ "Content-Type": "video/mp4",
+ },
+ },
+ {
+ uri: "file:///tmp/video.mp4",
+ name: "video.mp4",
+ type: "video/mp4",
+ size: 3,
+ },
+ onProgress,
+ );
+
+ expect(fileSystemMock.createUploadTask).toHaveBeenCalledWith(
+ "https://uploads.example/drive",
+ "file:///tmp/video.mp4",
+ {
+ headers: {
+ "Content-Range": "bytes 0-2/3",
+ "Content-Type": "video/mp4",
+ },
+ httpMethod: "PUT",
+ uploadType: fileSystemMock.FileSystemUploadType.BINARY_CONTENT,
+ },
+ expect.any(Function),
+ );
+ expect(uploadAsync).toHaveBeenCalled();
+ expect(onProgress).toHaveBeenCalledWith({ loaded: 2, total: 3 });
+ });
+
+ it("sets the Drive resumable upload byte range for remote blobs", async () => {
+ class MockXMLHttpRequest {
+ static instances: MockXMLHttpRequest[] = [];
+ upload: {
+ onprogress:
+ | ((event: ProgressEvent) => void)
+ | null;
+ } = { onprogress: null };
+ status = 200;
+ responseText = "";
+ onload: (() => void) | null = null;
+ onerror: (() => void) | null = null;
+ method = "";
+ url = "";
+ headers = new Map();
+ body: BodyInit | null = null;
+
+ constructor() {
+ MockXMLHttpRequest.instances.push(this);
+ }
+
+ open(method: string, url: string) {
+ this.method = method;
+ this.url = url;
+ }
+
+ setRequestHeader(key: string, value: string) {
+ this.headers.set(key, value);
+ }
+
+ send(body: BodyInit) {
+ this.body = body;
+ this.onload?.();
+ }
+ }
+
+ const originalFetch = globalThis.fetch;
+ const originalXhr = globalThis.XMLHttpRequest;
+ globalThis.fetch = (async () =>
+ new Response(new Uint8Array([1, 2, 3]))) as typeof fetch;
+ globalThis.XMLHttpRequest =
+ MockXMLHttpRequest as unknown as typeof XMLHttpRequest;
+
+ try {
+ await uploadToTarget(
+ {
+ type: "driveResumable",
+ url: "https://uploads.example/drive",
+ headers: {
+ "Content-Type": "video/mp4",
+ },
+ },
+ {
+ uri: "https://cache.example/video.mp4",
+ name: "video.mp4",
+ type: "video/mp4",
+ size: 3,
+ },
+ );
+
+ const request = MockXMLHttpRequest.instances[0];
+ expect(request?.method).toBe("PUT");
+ expect(request?.headers.get("content-type")).toBe("video/mp4");
+ expect(request?.headers.get("content-range")).toBe("bytes 0-2/3");
+ } finally {
+ globalThis.fetch = originalFetch;
+ globalThis.XMLHttpRequest = originalXhr;
+ }
+ });
+});
diff --git a/apps/mobile/src/api/mobile.ts b/apps/mobile/src/api/mobile.ts
new file mode 100644
index 00000000000..d4d352469ac
--- /dev/null
+++ b/apps/mobile/src/api/mobile.ts
@@ -0,0 +1,479 @@
+import { Mobile, type Storage } from "@cap/web-domain";
+import { Schema } from "effect";
+import * as FileSystem from "expo-file-system/legacy";
+
+export type MobileApiKeyResponse = typeof Mobile.MobileApiKeyResponse.Type;
+export type MobileSuccessResponse = typeof Mobile.MobileSuccessResponse.Type;
+export type MobileAuthConfigResponse =
+ typeof Mobile.MobileAuthConfigResponse.Type;
+export type MobileBootstrapResponse =
+ typeof Mobile.MobileBootstrapResponse.Type;
+export type MobileCapsListResponse = typeof Mobile.MobileCapsListResponse.Type;
+export type MobileCapSummary = typeof Mobile.MobileCapSummary.Type;
+export type MobileFolder = typeof Mobile.MobileFolder.Type;
+export type MobileCapDetail = typeof Mobile.MobileCapDetail.Type;
+export type MobileComment = typeof Mobile.MobileComment.Type;
+export type MobilePlaybackResponse = typeof Mobile.MobilePlaybackResponse.Type;
+export type MobileDownloadResponse = typeof Mobile.MobileDownloadResponse.Type;
+export type MobileCapSharingInput = typeof Mobile.MobileCapSharingInput.Type;
+export type MobileCapTitleInput = typeof Mobile.MobileCapTitleInput.Type;
+export type MobileCapPasswordInput = typeof Mobile.MobileCapPasswordInput.Type;
+export type MobileFolderCreateInput =
+ typeof Mobile.MobileFolderCreateInput.Type;
+export type MobileUploadCreateInput =
+ typeof Mobile.MobileUploadCreateInput.Type;
+export type MobileUploadCreateResponse =
+ typeof Mobile.MobileUploadCreateResponse.Type;
+
+export type MobileApiClient = ReturnType;
+
+export type UploadFile = {
+ uri: string;
+ name: string;
+ type: string;
+ size?: number;
+ durationSeconds?: number;
+ width?: number;
+ height?: number;
+};
+
+export type UploadProgress = {
+ loaded: number;
+ total: number;
+};
+
+type ClientOptions = {
+ baseUrl: string;
+ getToken: () => string | Promise | null;
+};
+
+type RequestOptions = {
+ method?: "GET" | "POST" | "PATCH" | "DELETE";
+ query?: Record;
+ body?: unknown;
+};
+
+export class MobileApiError extends Error {
+ constructor(
+ message: string,
+ readonly status: number,
+ readonly payload: unknown,
+ ) {
+ super(message);
+ this.name = "MobileApiError";
+ }
+}
+
+const trimBaseUrl = (baseUrl: string) => baseUrl.replace(/\/+$/, "");
+
+const decode = async (
+ schema: Schema.Schema,
+ value: unknown,
+): Promise => Schema.decodeUnknownPromise(schema)(value);
+
+const appendQuery = (
+ url: URL,
+ query: Record | undefined,
+) => {
+ if (!query) return;
+ for (const [key, value] of Object.entries(query)) {
+ if (value !== null && value !== undefined && value !== "") {
+ url.searchParams.set(key, String(value));
+ }
+ }
+};
+
+const parseJson = async (response: Response) => {
+ const text = await response.text();
+ if (text.length === 0) return null;
+ return JSON.parse(text) as unknown;
+};
+
+export const createSessionRequestUrl = (
+ baseUrl: string,
+ redirectUri: string,
+ provider?: "google" | "workos",
+ organizationId?: string,
+) => {
+ const url = new URL("/api/mobile/session/request", trimBaseUrl(baseUrl));
+ url.searchParams.set("redirectUri", redirectUri);
+ if (provider) url.searchParams.set("provider", provider);
+ if (organizationId) url.searchParams.set("organizationId", organizationId);
+ return url.toString();
+};
+
+export const createMobileApiClient = ({ baseUrl, getToken }: ClientOptions) => {
+ const origin = trimBaseUrl(baseUrl);
+
+ const request = async (
+ path: string,
+ schema: Schema.Schema,
+ options: RequestOptions = {},
+ ): Promise => {
+ const token = await getToken();
+ if (!token) {
+ throw new MobileApiError("Missing mobile session", 401, null);
+ }
+
+ const url = new URL(path, origin);
+ appendQuery(url, options.query);
+ const headers = new Headers({
+ Authorization: `Bearer ${token}`,
+ });
+ let body: BodyInit | undefined;
+ if (options.body !== undefined) {
+ headers.set("Content-Type", "application/json");
+ body = JSON.stringify(options.body);
+ }
+
+ const response = await fetch(url.toString(), {
+ method: options.method ?? "GET",
+ headers,
+ body,
+ });
+ const payload = await parseJson(response);
+ if (!response.ok) {
+ throw new MobileApiError(
+ `Mobile API request failed with ${response.status}`,
+ response.status,
+ payload,
+ );
+ }
+ return decode(schema, payload);
+ };
+
+ const publicRequest = async (
+ path: string,
+ schema: Schema.Schema,
+ options: Omit = {},
+ ): Promise => {
+ const url = new URL(path, origin);
+ const headers = new Headers();
+ let body: BodyInit | undefined;
+ if (options.body !== undefined) {
+ headers.set("Content-Type", "application/json");
+ body = JSON.stringify(options.body);
+ }
+
+ const response = await fetch(url.toString(), {
+ method: options.method ?? "GET",
+ headers,
+ body,
+ });
+ const payload = await parseJson(response);
+ if (!response.ok) {
+ throw new MobileApiError(
+ `Mobile API request failed with ${response.status}`,
+ response.status,
+ payload,
+ );
+ }
+ return decode(schema, payload);
+ };
+
+ return {
+ getAuthConfig: () =>
+ publicRequest(
+ "/api/mobile/session/config",
+ Mobile.MobileAuthConfigResponse,
+ ),
+ requestEmailCode: (email: string) =>
+ publicRequest(
+ "/api/mobile/session/email/request",
+ Mobile.MobileSuccessResponse,
+ {
+ method: "POST",
+ body: { email },
+ },
+ ),
+ verifyEmailCode: (input: { email: string; code: string }) =>
+ publicRequest(
+ "/api/mobile/session/email/verify",
+ Mobile.MobileApiKeyResponse,
+ {
+ method: "POST",
+ body: input,
+ },
+ ),
+ bootstrap: () =>
+ request("/api/mobile/bootstrap", Mobile.MobileBootstrapResponse),
+ setActiveOrganization: (organizationId: string) =>
+ request(
+ "/api/mobile/user/active-organization",
+ Mobile.MobileBootstrapResponse,
+ {
+ method: "PATCH",
+ body: { organizationId },
+ },
+ ),
+ listCaps: (params: {
+ folderId?: string | null;
+ page?: number;
+ limit?: number;
+ }) =>
+ request("/api/mobile/caps", Mobile.MobileCapsListResponse, {
+ query: params,
+ }),
+ createFolder: (input: MobileFolderCreateInput) =>
+ request("/api/mobile/folders", Mobile.MobileFolder, {
+ method: "POST",
+ body: input,
+ }),
+ getCap: (id: string) =>
+ request(`/api/mobile/caps/${id}`, Mobile.MobileCapDetail),
+ updateCapSharing: (id: string, input: MobileCapSharingInput) =>
+ request(`/api/mobile/caps/${id}/sharing`, Mobile.MobileCapSummary, {
+ method: "PATCH",
+ body: input,
+ }),
+ updateCapTitle: (id: string, input: MobileCapTitleInput) =>
+ request(`/api/mobile/caps/${id}/title`, Mobile.MobileCapSummary, {
+ method: "PATCH",
+ body: input,
+ }),
+ updateCapPassword: (id: string, input: MobileCapPasswordInput) =>
+ request(`/api/mobile/caps/${id}/password`, Mobile.MobileCapSummary, {
+ method: "PATCH",
+ body: input,
+ }),
+ deleteCap: (id: string) =>
+ request(`/api/mobile/caps/${id}`, Mobile.MobileSuccessResponse, {
+ method: "DELETE",
+ }),
+ getPlayback: (id: string) =>
+ request(`/api/mobile/caps/${id}/playback`, Mobile.MobilePlaybackResponse),
+ getDownload: (id: string) =>
+ request(`/api/mobile/caps/${id}/download`, Mobile.MobileDownloadResponse),
+ createComment: (
+ id: string,
+ input: { content: string; timestamp: number | null },
+ ) =>
+ request(`/api/mobile/caps/${id}/comments`, Mobile.MobileComment, {
+ method: "POST",
+ body: input,
+ }),
+ deleteComment: (id: string) =>
+ request(`/api/mobile/comments/${id}`, Mobile.MobileSuccessResponse, {
+ method: "DELETE",
+ }),
+ createReaction: (
+ id: string,
+ input: { content: string; timestamp: number | null },
+ ) =>
+ request(`/api/mobile/caps/${id}/reactions`, Mobile.MobileComment, {
+ method: "POST",
+ body: input,
+ }),
+ createUpload: (input: MobileUploadCreateInput) =>
+ request("/api/mobile/uploads", Mobile.MobileUploadCreateResponse, {
+ method: "POST",
+ body: input,
+ }),
+ updateUploadProgress: (
+ id: string,
+ input: { uploaded: number; total: number },
+ ) =>
+ request(
+ `/api/mobile/uploads/${id}/progress`,
+ Mobile.MobileSuccessResponse,
+ {
+ method: "POST",
+ body: input,
+ },
+ ),
+ completeUpload: (
+ id: string,
+ input: { rawFileKey: string; contentLength?: number },
+ ) =>
+ request(
+ `/api/mobile/uploads/${id}/complete`,
+ Mobile.MobileSuccessResponse,
+ {
+ method: "POST",
+ body: input,
+ },
+ ),
+ revokeSession: () =>
+ request("/api/mobile/session/revoke", Mobile.MobileSuccessResponse, {
+ method: "POST",
+ }),
+ };
+};
+
+const targetHeaders = (headers: Record) => {
+ const result = new Headers();
+ for (const [key, value] of Object.entries(headers)) {
+ result.set(key, value);
+ }
+ return result;
+};
+
+const isNativeUploadUri = (uri: string) =>
+ uri.startsWith("file://") || uri.startsWith("content://");
+
+const getLocalFileSize = async (file: UploadFile) => {
+ if (typeof file.size === "number" && file.size > 0) return file.size;
+
+ const info = await FileSystem.getInfoAsync(file.uri);
+ if (!info.exists || info.isDirectory) return 0;
+ return info.size;
+};
+
+const uploadNativeFile = async (
+ method: "POST" | "PUT",
+ url: string,
+ file: UploadFile,
+ options: FileSystem.FileSystemUploadOptions,
+ onProgress?: (progress: UploadProgress) => void,
+) => {
+ const task = FileSystem.createUploadTask(
+ url,
+ file.uri,
+ {
+ ...options,
+ httpMethod: method,
+ },
+ (data) => {
+ onProgress?.({
+ loaded: data.totalBytesSent,
+ total: data.totalBytesExpectedToSend,
+ });
+ },
+ );
+ const response = await task.uploadAsync();
+ if (!response || response.status < 200 || response.status >= 300) {
+ throw new MobileApiError(
+ "Upload target rejected the file",
+ response?.status ?? 0,
+ response?.body ?? null,
+ );
+ }
+};
+
+const uploadWithXhr = (
+ method: "POST" | "PUT",
+ url: string,
+ headers: Headers,
+ body: FormData | Blob,
+ onProgress?: (progress: UploadProgress) => void,
+) =>
+ new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open(method, url);
+ headers.forEach((value, key) => {
+ xhr.setRequestHeader(key, value);
+ });
+ xhr.upload.onprogress = (event) => {
+ onProgress?.({
+ loaded: event.loaded,
+ total: event.lengthComputable ? event.total : 0,
+ });
+ };
+ xhr.onload = () => {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ resolve();
+ return;
+ }
+ reject(
+ new MobileApiError(
+ "Upload target rejected the file",
+ xhr.status,
+ xhr.responseText,
+ ),
+ );
+ };
+ xhr.onerror = () => {
+ reject(new Error("Upload failed"));
+ };
+ xhr.send(body);
+ });
+
+const fileBlob = async (file: UploadFile) => {
+ const response = await fetch(file.uri);
+ return response.blob();
+};
+
+export const uploadToTarget = async (
+ target: Storage.UploadTarget,
+ file: UploadFile,
+ onProgress?: (progress: UploadProgress) => void,
+) => {
+ if (target.type === "s3Post") {
+ if (isNativeUploadUri(file.uri)) {
+ await uploadNativeFile(
+ "POST",
+ target.url,
+ file,
+ {
+ fieldName: "file",
+ mimeType: file.type,
+ parameters: target.fields,
+ uploadType: FileSystem.FileSystemUploadType.MULTIPART,
+ },
+ onProgress,
+ );
+ return;
+ }
+
+ const formData = new FormData();
+ for (const [key, value] of Object.entries(target.fields)) {
+ formData.append(key, value);
+ }
+ formData.append("file", {
+ uri: file.uri,
+ name: file.name,
+ type: file.type,
+ } as unknown as Blob);
+ await uploadWithXhr(
+ "POST",
+ target.url,
+ new Headers(),
+ formData,
+ onProgress,
+ );
+ return;
+ }
+
+ const headers = { ...target.headers };
+ let size = file.size;
+ if (
+ target.type === "driveResumable" &&
+ typeof size === "number" &&
+ size > 0
+ ) {
+ headers["Content-Range"] = `bytes 0-${size - 1}/${size}`;
+ }
+
+ if (isNativeUploadUri(file.uri)) {
+ if (target.type === "driveResumable" && !size) {
+ size = await getLocalFileSize(file);
+ if (size > 0) {
+ headers["Content-Range"] = `bytes 0-${size - 1}/${size}`;
+ }
+ }
+
+ await uploadNativeFile(
+ "PUT",
+ target.url,
+ file,
+ {
+ headers,
+ uploadType: FileSystem.FileSystemUploadType.BINARY_CONTENT,
+ },
+ onProgress,
+ );
+ return;
+ }
+
+ const blob = await fileBlob(file);
+ if (target.type === "driveResumable" && !size && blob.size > 0) {
+ headers["Content-Range"] = `bytes 0-${blob.size - 1}/${blob.size}`;
+ }
+ await uploadWithXhr(
+ "PUT",
+ target.url,
+ targetHeaders(headers),
+ blob,
+ onProgress,
+ );
+};
diff --git a/apps/mobile/src/auth/AuthContext.test.ts b/apps/mobile/src/auth/AuthContext.test.ts
new file mode 100644
index 00000000000..e0bc13805f3
--- /dev/null
+++ b/apps/mobile/src/auth/AuthContext.test.ts
@@ -0,0 +1,31 @@
+import { describe, expect, it } from "vitest";
+import { parseAuthRedirect, requireAuthRedirectSession } from "./session";
+
+describe("parseAuthRedirect", () => {
+ it("extracts the issued API key and user id", () => {
+ expect(
+ parseAuthRedirect("cap://auth?api_key=key_123&user_id=user_123"),
+ ).toEqual({
+ apiKey: "key_123",
+ userId: "user_123",
+ });
+ });
+
+ it("rejects redirects without an API key", () => {
+ expect(parseAuthRedirect("cap://auth?user_id=user_123")).toBeNull();
+ });
+
+ it("throws a usable message for failed auth callbacks", () => {
+ expect(() =>
+ requireAuthRedirectSession(
+ "cap://auth?error_description=Organization%20not%20found",
+ ),
+ ).toThrow("Organization not found");
+ });
+
+ it("throws when an auth callback omits the mobile API key", () => {
+ expect(() =>
+ requireAuthRedirectSession("cap://auth?user_id=user_123"),
+ ).toThrow("Sign in did not return a mobile session.");
+ });
+});
diff --git a/apps/mobile/src/auth/AuthContext.tsx b/apps/mobile/src/auth/AuthContext.tsx
new file mode 100644
index 00000000000..9c97d7ec2e5
--- /dev/null
+++ b/apps/mobile/src/auth/AuthContext.tsx
@@ -0,0 +1,256 @@
+import Constants from "expo-constants";
+import * as Linking from "expo-linking";
+import * as SecureStore from "expo-secure-store";
+import * as WebBrowser from "expo-web-browser";
+import {
+ createContext,
+ type ReactNode,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from "react";
+import {
+ createMobileApiClient,
+ createSessionRequestUrl,
+ type MobileApiClient,
+ type MobileAuthConfigResponse,
+ type MobileBootstrapResponse,
+} from "@/api/mobile";
+import { requireAuthRedirectSession } from "./session";
+
+WebBrowser.maybeCompleteAuthSession();
+
+const sessionKey = "cap.mobile.apiKey";
+const userIdKey = "cap.mobile.userId";
+
+type AuthState = {
+ status: "loading" | "signedOut" | "signedIn";
+ apiKey: string | null;
+ userId: string | null;
+ authConfig: MobileAuthConfigResponse;
+ bootstrap: MobileBootstrapResponse | null;
+ client: MobileApiClient;
+ requestEmailCode: (email: string) => Promise;
+ verifyEmailCode: (email: string, code: string) => Promise;
+ signInWithGoogle: () => Promise;
+ signInWithSso: (organizationId: string) => Promise;
+ signOut: () => Promise;
+ refresh: () => Promise;
+ setActiveOrganization: (organizationId: string) => Promise;
+};
+
+const AuthContext = createContext(null);
+const fallbackAuthConfig: MobileAuthConfigResponse = {
+ googleAuthAvailable: true,
+ workosAuthAvailable: true,
+};
+
+const getExtraString = (key: string, fallback: string) => {
+ const extra = Constants.expoConfig?.extra;
+ if (!extra || typeof extra !== "object") return fallback;
+ const value = (extra as Record)[key];
+ return typeof value === "string" ? value : fallback;
+};
+
+export const apiBaseUrl = getExtraString("apiBaseUrl", "https://cap.so");
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+ const [apiKey, setApiKey] = useState(null);
+ const [userId, setUserId] = useState(null);
+ const [authConfig, setAuthConfig] =
+ useState(fallbackAuthConfig);
+ const [bootstrap, setBootstrap] = useState(
+ null,
+ );
+ const [loading, setLoading] = useState(true);
+
+ const client = useMemo(
+ () =>
+ createMobileApiClient({
+ baseUrl: apiBaseUrl,
+ getToken: () => apiKey,
+ }),
+ [apiKey],
+ );
+ const publicClient = useMemo(
+ () =>
+ createMobileApiClient({
+ baseUrl: apiBaseUrl,
+ getToken: () => null,
+ }),
+ [],
+ );
+
+ const refresh = useCallback(async () => {
+ const response = await client.bootstrap();
+ setBootstrap(response);
+ }, [client]);
+
+ useEffect(() => {
+ let active = true;
+ const load = async () => {
+ try {
+ const [storedKey, storedUserId, nextAuthConfig] = await Promise.all([
+ SecureStore.getItemAsync(sessionKey),
+ SecureStore.getItemAsync(userIdKey),
+ publicClient.getAuthConfig().catch(() => fallbackAuthConfig),
+ ]);
+ if (!active) return;
+ setApiKey(storedKey);
+ setUserId(storedKey ? storedUserId : null);
+ setAuthConfig(nextAuthConfig);
+ if (!storedKey && storedUserId) {
+ SecureStore.deleteItemAsync(userIdKey).catch(() => {});
+ }
+ } finally {
+ if (active) setLoading(false);
+ }
+ };
+ load();
+ return () => {
+ active = false;
+ };
+ }, [publicClient]);
+
+ useEffect(() => {
+ if (!apiKey) {
+ setUserId(null);
+ setBootstrap(null);
+ return;
+ }
+
+ refresh().catch(() => {
+ setApiKey(null);
+ setUserId(null);
+ setBootstrap(null);
+ SecureStore.deleteItemAsync(sessionKey).catch(() => {});
+ SecureStore.deleteItemAsync(userIdKey).catch(() => {});
+ });
+ }, [apiKey, refresh]);
+
+ const storeSession = useCallback(
+ async (session: { apiKey: string; userId: string | null }) => {
+ await SecureStore.setItemAsync(sessionKey, session.apiKey);
+ if (session.userId) {
+ await SecureStore.setItemAsync(userIdKey, session.userId);
+ } else {
+ await SecureStore.deleteItemAsync(userIdKey);
+ }
+ setApiKey(session.apiKey);
+ setUserId(session.userId);
+ },
+ [],
+ );
+
+ const requestEmailCode = useCallback(
+ async (email: string) => {
+ await client.requestEmailCode(email);
+ },
+ [client],
+ );
+
+ const verifyEmailCode = useCallback(
+ async (email: string, code: string) => {
+ const session = await client.verifyEmailCode({ email, code });
+ await storeSession({
+ apiKey: session.apiKey,
+ userId: session.userId,
+ });
+ },
+ [client, storeSession],
+ );
+
+ const signInWithGoogle = useCallback(async () => {
+ const redirectUri = Linking.createURL("auth");
+ const result = await WebBrowser.openAuthSessionAsync(
+ createSessionRequestUrl(apiBaseUrl, redirectUri, "google"),
+ redirectUri,
+ );
+ if (result.type !== "success") return;
+
+ await storeSession(requireAuthRedirectSession(result.url));
+ }, [storeSession]);
+
+ const signInWithSso = useCallback(
+ async (organizationId: string) => {
+ const redirectUri = Linking.createURL("auth");
+ const result = await WebBrowser.openAuthSessionAsync(
+ createSessionRequestUrl(
+ apiBaseUrl,
+ redirectUri,
+ "workos",
+ organizationId,
+ ),
+ redirectUri,
+ );
+ if (result.type !== "success") return;
+
+ await storeSession(requireAuthRedirectSession(result.url));
+ },
+ [storeSession],
+ );
+
+ const signOut = useCallback(async () => {
+ if (apiKey) {
+ await client.revokeSession().catch(() => {});
+ }
+ await Promise.all([
+ SecureStore.deleteItemAsync(sessionKey),
+ SecureStore.deleteItemAsync(userIdKey),
+ ]);
+ setApiKey(null);
+ setUserId(null);
+ setBootstrap(null);
+ }, [apiKey, client]);
+
+ const setActiveOrganization = useCallback(
+ async (organizationId: string) => {
+ const nextBootstrap = await client.setActiveOrganization(organizationId);
+ setBootstrap(nextBootstrap);
+ },
+ [client],
+ );
+
+ const value = useMemo(
+ () => ({
+ status: loading ? "loading" : apiKey ? "signedIn" : "signedOut",
+ apiKey,
+ userId,
+ authConfig,
+ bootstrap,
+ client,
+ requestEmailCode,
+ verifyEmailCode,
+ signInWithGoogle,
+ signInWithSso,
+ signOut,
+ refresh,
+ setActiveOrganization,
+ }),
+ [
+ loading,
+ apiKey,
+ userId,
+ authConfig,
+ bootstrap,
+ client,
+ requestEmailCode,
+ verifyEmailCode,
+ signInWithGoogle,
+ signInWithSso,
+ signOut,
+ refresh,
+ setActiveOrganization,
+ ],
+ );
+
+ return {children};
+}
+
+export const useAuth = () => {
+ const value = useContext(AuthContext);
+ if (!value) throw new Error("useAuth must be used inside AuthProvider");
+ return value;
+};
diff --git a/apps/mobile/src/auth/AuthProvider.test.tsx b/apps/mobile/src/auth/AuthProvider.test.tsx
new file mode 100644
index 00000000000..0d9c232f2b3
--- /dev/null
+++ b/apps/mobile/src/auth/AuthProvider.test.tsx
@@ -0,0 +1,172 @@
+import React, { type ReactNode } from "react";
+import TestRenderer, { act } from "react-test-renderer";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { AuthProvider, useAuth } from "./AuthContext";
+
+type HostProps = {
+ children?: ReactNode;
+};
+
+const secureStoreMock = vi.hoisted(() => ({
+ deleteItemAsync: vi.fn((_key: string) => Promise.resolve()),
+ getItemAsync: vi.fn((_key: string) => Promise.resolve(null as string | null)),
+ setItemAsync: vi.fn((_key: string, _value: string) => Promise.resolve()),
+}));
+
+const apiMock = vi.hoisted(() => ({
+ bootstrap: vi.fn(() =>
+ Promise.resolve({
+ activeOrganizationId: "org_123",
+ user: {
+ email: "richie@cap.so",
+ name: "Richie",
+ },
+ }),
+ ),
+ getAuthConfig: vi.fn(() =>
+ Promise.resolve({
+ googleAuthAvailable: true,
+ workosAuthAvailable: true,
+ }),
+ ),
+}));
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+
+ return {
+ View: ({ children }: HostProps) =>
+ React.createElement("View", null, children),
+ };
+});
+
+vi.mock("expo-constants", () => ({
+ default: {
+ expoConfig: {
+ extra: {},
+ },
+ },
+}));
+
+vi.mock("expo-linking", () => ({
+ createURL: vi.fn(() => "cap://auth"),
+}));
+
+vi.mock("expo-secure-store", () => secureStoreMock);
+
+vi.mock("expo-web-browser", () => ({
+ maybeCompleteAuthSession: vi.fn(),
+ openAuthSessionAsync: vi.fn(),
+}));
+
+vi.mock("@/api/mobile", () => ({
+ createMobileApiClient: vi.fn(() => ({
+ bootstrap: apiMock.bootstrap,
+ getAuthConfig: apiMock.getAuthConfig,
+ revokeSession: vi.fn(() => Promise.resolve({ success: true })),
+ setActiveOrganization: vi.fn(),
+ })),
+ createSessionRequestUrl: vi.fn(
+ () => "https://cap.so/api/mobile/session/request",
+ ),
+}));
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const flushMicrotasks = async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ await Promise.resolve();
+ await Promise.resolve();
+};
+
+const states: Array<{
+ apiKey: string | null;
+ status: string;
+ userId: string | null;
+}> = [];
+
+const Probe = () => {
+ const auth = useAuth();
+ states.push({
+ apiKey: auth.apiKey,
+ status: auth.status,
+ userId: auth.userId,
+ });
+ return null;
+};
+
+describe("AuthProvider", () => {
+ beforeEach(() => {
+ states.length = 0;
+ secureStoreMock.deleteItemAsync.mockClear();
+ secureStoreMock.getItemAsync.mockReset();
+ secureStoreMock.getItemAsync.mockResolvedValue(null);
+ secureStoreMock.setItemAsync.mockClear();
+ apiMock.bootstrap.mockReset();
+ apiMock.bootstrap.mockResolvedValue({
+ activeOrganizationId: "org_123",
+ user: {
+ email: "richie@cap.so",
+ name: "Richie",
+ },
+ });
+ apiMock.getAuthConfig.mockReset();
+ apiMock.getAuthConfig.mockResolvedValue({
+ googleAuthAvailable: true,
+ workosAuthAvailable: true,
+ });
+ });
+
+ it("clears an orphaned stored user id when no API key is stored", async () => {
+ secureStoreMock.getItemAsync.mockImplementation((key: string) =>
+ Promise.resolve(key === "cap.mobile.userId" ? "user_123" : null),
+ );
+
+ await act(async () => {
+ TestRenderer.create(
+ React.createElement(AuthProvider, null, React.createElement(Probe)),
+ );
+ await flushMicrotasks();
+ });
+
+ expect(states.at(-1)).toMatchObject({
+ apiKey: null,
+ status: "signedOut",
+ userId: null,
+ });
+ expect(secureStoreMock.deleteItemAsync).toHaveBeenCalledWith(
+ "cap.mobile.userId",
+ );
+ });
+
+ it("clears the stored user id when bootstrapping a stored session fails", async () => {
+ secureStoreMock.getItemAsync.mockImplementation((key: string) => {
+ if (key === "cap.mobile.apiKey") return Promise.resolve("key_123");
+ if (key === "cap.mobile.userId") return Promise.resolve("user_123");
+ return Promise.resolve(null);
+ });
+ apiMock.bootstrap.mockRejectedValueOnce(new Error("Session expired"));
+
+ await act(async () => {
+ TestRenderer.create(
+ React.createElement(AuthProvider, null, React.createElement(Probe)),
+ );
+ await flushMicrotasks();
+ });
+
+ expect(states.at(-1)).toMatchObject({
+ apiKey: null,
+ status: "signedOut",
+ userId: null,
+ });
+ expect(secureStoreMock.deleteItemAsync).toHaveBeenCalledWith(
+ "cap.mobile.apiKey",
+ );
+ expect(secureStoreMock.deleteItemAsync).toHaveBeenCalledWith(
+ "cap.mobile.userId",
+ );
+ });
+});
diff --git a/apps/mobile/src/auth/SignInPanel.test.tsx b/apps/mobile/src/auth/SignInPanel.test.tsx
new file mode 100644
index 00000000000..9970dfb9cb4
--- /dev/null
+++ b/apps/mobile/src/auth/SignInPanel.test.tsx
@@ -0,0 +1,1234 @@
+import React, { type ReactElement, type ReactNode } from "react";
+import TestRenderer, {
+ act,
+ type ReactTestRenderer,
+ type ReactTestRendererJSON,
+} from "react-test-renderer";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { SignInPanel } from "./SignInPanel";
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+type JsonNode = ReactTestRendererJSON | ReactTestRendererJSON[] | string | null;
+
+const authFns = vi.hoisted(() => ({
+ authConfig: {
+ googleAuthAvailable: true,
+ workosAuthAvailable: true,
+ },
+ requestEmailCode: vi.fn(() => Promise.resolve()),
+ signInWithGoogle: vi.fn(),
+ signInWithSso: vi.fn(),
+ verifyEmailCode: vi.fn(),
+}));
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const renderTree = async (node: ReactElement): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return (renderer as ReactTestRenderer | null)?.toJSON() ?? null;
+};
+
+const renderPanel = async (): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(React.createElement(SignInPanel));
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+const getTextNodes = (node: JsonNode): string[] => {
+ if (!node) return [];
+ if (typeof node === "string") return [node];
+ if (Array.isArray(node)) return node.flatMap(getTextNodes);
+ return node.children?.flatMap(getTextNodes) ?? [];
+};
+
+const hasProp = (node: JsonNode, prop: string, value: unknown): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node))
+ return node.some((item) => hasProp(item, prop, value));
+ if (node.props[prop] === value) return true;
+ return node.children?.some((child) => hasProp(child, prop, value)) ?? false;
+};
+
+const hasStyle = (
+ node: JsonNode,
+ expected: Record,
+): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node)) return node.some((item) => hasStyle(item, expected));
+ const styles = Array.isArray(node.props.style)
+ ? node.props.style
+ : [node.props.style];
+ const resolved = Object.assign({}, ...styles.filter(Boolean));
+ if (
+ Object.entries(expected).every(([key, value]) => resolved[key] === value)
+ ) {
+ return true;
+ }
+ return node.children?.some((child) => hasStyle(child, expected)) ?? false;
+};
+
+const resolveStyle = (
+ style: unknown,
+ pressed = false,
+): Record => {
+ const resolved = typeof style === "function" ? style({ pressed }) : style;
+ const styles = Array.isArray(resolved) ? resolved : [resolved];
+ return Object.assign({}, ...styles.filter(Boolean));
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+ const TextInput = React.forwardRef(
+ ({ children, ...props }, ref) =>
+ React.createElement(
+ "TextInput",
+ { ...props, ref },
+ children as ReactNode,
+ ),
+ );
+
+ return {
+ ActivityIndicator: createHost("ActivityIndicator"),
+ Pressable: createHost("Pressable"),
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ hairlineWidth: 1,
+ },
+ Text: createHost("Text"),
+ TextInput,
+ View: createHost("View"),
+ };
+});
+
+vi.mock("expo-symbols", () => ({
+ SymbolView: () => null,
+}));
+
+vi.mock("expo-web-browser", () => ({
+ openBrowserAsync: vi.fn(),
+}));
+
+vi.mock("@/components/GlassSurface", async () => {
+ const React = await import("react");
+ return {
+ GlassSurface: ({ children }: { children?: ReactNode }) =>
+ React.createElement("GlassSurface", null, children),
+ };
+});
+
+vi.mock("react-native-svg", async () => {
+ const React = await import("react");
+ const Svg = ({ children, ...props }: HostProps) =>
+ React.createElement("Svg", props, children);
+
+ return {
+ default: Svg,
+ Path: (props: HostProps) => React.createElement("Path", props),
+ Rect: (props: HostProps) => React.createElement("Rect", props),
+ };
+});
+
+vi.mock("@/auth/AuthContext", () => ({
+ apiBaseUrl: "https://cap.so",
+ useAuth: () => ({
+ authConfig: authFns.authConfig,
+ requestEmailCode: authFns.requestEmailCode,
+ signInWithGoogle: authFns.signInWithGoogle,
+ signInWithSso: authFns.signInWithSso,
+ verifyEmailCode: authFns.verifyEmailCode,
+ }),
+}));
+
+vi.mock("@/api/mobile", () => ({
+ MobileApiError: class MobileApiError extends Error {
+ status: number;
+ payload: unknown;
+
+ constructor(message: string, status: number, payload: unknown) {
+ super(message);
+ this.status = status;
+ this.payload = payload;
+ }
+ },
+}));
+
+describe("SignInPanel", () => {
+ beforeEach(() => {
+ authFns.authConfig.googleAuthAvailable = true;
+ authFns.authConfig.workosAuthAvailable = true;
+ authFns.requestEmailCode.mockReset();
+ authFns.requestEmailCode.mockResolvedValue(undefined);
+ authFns.verifyEmailCode.mockReset();
+ authFns.verifyEmailCode.mockResolvedValue(undefined);
+ authFns.signInWithGoogle.mockReset();
+ authFns.signInWithGoogle.mockResolvedValue(undefined);
+ authFns.signInWithSso.mockReset();
+ authFns.signInWithSso.mockResolvedValue(undefined);
+ });
+
+ it("renders the Cap web login surface", async () => {
+ const tree = await renderTree(React.createElement(SignInPanel));
+ const text = getTextNodes(tree);
+
+ expect(text).toContain("Sign in to Cap");
+ expect(text).toContain("Your videos, organized and ready to share.");
+ expect(hasProp(tree, "viewBox", "0 0 40 40")).toBe(true);
+ expect(hasProp(tree, "rx", 8)).toBe(true);
+ expect(hasProp(tree, "placeholder", "tim@apple.com")).toBe(true);
+ expect(hasProp(tree, "accessibilityLabel", "Email address")).toBe(true);
+ expect(
+ hasProp(
+ tree,
+ "accessibilityHint",
+ "Enter your email to request a verification code",
+ ),
+ ).toBe(true);
+ expect(hasProp(tree, "clearButtonMode", "while-editing")).toBe(true);
+ expect(hasProp(tree, "enablesReturnKeyAutomatically", true)).toBe(true);
+ expect(hasProp(tree, "selectionColor", "#0090ff")).toBe(true);
+ expect(
+ hasProp(
+ tree,
+ "accessibilityHint",
+ "Enter a valid email address to continue",
+ ),
+ ).toBe(true);
+ expect(text).toContain("Login with email");
+ expect(text).toContain("Sign up here");
+ expect(
+ hasProp(tree, "accessibilityHint", "Opens sign up in a browser sheet"),
+ ).toBe(true);
+ expect(hasProp(tree, "accessibilityRole", "link")).toBe(true);
+ expect(text).toContain("OR");
+ expect(text).toContain("Login with Google");
+ expect(text).toContain("Login with SAML SSO");
+ expect(text).toContain("Terms of Service");
+ expect(text).toContain("Privacy Policy");
+ expect(
+ hasProp(
+ tree,
+ "accessibilityHint",
+ "Opens Terms of Service in a browser sheet",
+ ),
+ ).toBe(true);
+ expect(
+ hasProp(
+ tree,
+ "accessibilityHint",
+ "Opens Privacy Policy in a browser sheet",
+ ),
+ ).toBe(true);
+ });
+
+ it("hides unavailable provider options", async () => {
+ authFns.authConfig.googleAuthAvailable = false;
+ authFns.authConfig.workosAuthAvailable = false;
+
+ const tree = await renderTree(React.createElement(SignInPanel));
+ const text = getTextNodes(tree);
+
+ expect(text).toContain("Login with email");
+ expect(text).not.toContain("OR");
+ expect(text).not.toContain("Login with Google");
+ expect(text).not.toContain("Login with SAML SSO");
+ });
+
+ it("shows the native SSO organization step", async () => {
+ const renderer = await renderPanel();
+ const [ssoButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with SAML SSO",
+ });
+ if (!ssoButton) throw new Error("SSO button was not rendered");
+
+ await act(async () => {
+ ssoButton.props.onPress();
+ });
+
+ const tree = renderer.toJSON();
+ const text = getTextNodes(tree);
+
+ expect(hasProp(tree, "placeholder", "Enter your Organization ID...")).toBe(
+ true,
+ );
+ expect(hasProp(tree, "accessibilityLabel", "Organization ID")).toBe(true);
+ expect(
+ hasProp(
+ tree,
+ "accessibilityHint",
+ "Enter your organization ID to continue with SSO",
+ ),
+ ).toBe(true);
+ expect(hasProp(tree, "clearButtonMode", "while-editing")).toBe(true);
+ expect(hasProp(tree, "selectionColor", "#0090ff")).toBe(true);
+ expect(
+ hasProp(
+ tree,
+ "accessibilityHint",
+ "Enter your organization ID to continue",
+ ),
+ ).toBe(true);
+ const [continueButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Continue with SSO",
+ });
+ expect(continueButton?.props.accessibilityValue).toEqual({
+ text: "Organization ID required",
+ });
+ expect(text).toContain("Continue with SSO");
+ expect(text).toContain("Back");
+ });
+
+ it("locks the SSO back button while starting sign in", async () => {
+ let resolveSso: (() => void) | null = null;
+ authFns.signInWithSso.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveSso = resolve;
+ }),
+ );
+ const renderer = await renderPanel();
+ const [ssoButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with SAML SSO",
+ });
+ if (!ssoButton) throw new Error("SSO button was not rendered");
+
+ await act(async () => {
+ ssoButton.props.onPress();
+ });
+
+ const [organizationInput] = renderer.root.findAllByProps({
+ accessibilityLabel: "Organization ID",
+ });
+ if (!organizationInput)
+ throw new Error("Organization ID input was not rendered");
+ await act(async () => {
+ organizationInput.props.onChangeText("acme");
+ });
+
+ const [continueButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Continue with SSO",
+ });
+ if (!continueButton)
+ throw new Error("SSO continue button was not rendered");
+ expect(continueButton.props.accessibilityHint).toBe(
+ "Starts SAML SSO for this organization",
+ );
+ await act(async () => {
+ void continueButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [loadingBackButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Back",
+ });
+ const [loadingOrganizationInput] = renderer.root.findAllByProps({
+ accessibilityLabel: "Organization ID",
+ });
+ const [loadingContinueButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Continue with SSO",
+ });
+ expect(loadingBackButton?.props.disabled).toBe(true);
+ expect(loadingBackButton?.props.accessibilityState).toEqual({
+ disabled: true,
+ });
+ expect(loadingBackButton?.props.accessibilityHint).toBe(
+ "Sign in is in progress",
+ );
+ expect(loadingBackButton?.props.accessibilityValue).toEqual({
+ text: "Starting SAML SSO sign in",
+ });
+ expect(loadingOrganizationInput?.props.editable).toBe(false);
+ expect(loadingOrganizationInput?.props.accessibilityState).toEqual({
+ disabled: true,
+ });
+ expect(loadingOrganizationInput?.props.accessibilityValue).toEqual({
+ text: "Starting SAML SSO sign in",
+ });
+ expect(loadingContinueButton?.props.accessibilityHint).toBe(
+ "SAML SSO sign in is starting",
+ );
+ expect(loadingContinueButton?.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(loadingContinueButton?.props.accessibilityValue).toEqual({
+ text: "Starting SAML SSO sign in",
+ });
+ expect(getTextNodes(renderer.toJSON())).toContain("Continue with SSO");
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Starting SSO...");
+
+ await act(async () => {
+ organizationInput.props.onChangeText("changed");
+ });
+
+ const [unchangedOrganizationInput] = renderer.root.findAllByProps({
+ accessibilityLabel: "Organization ID",
+ });
+ expect(unchangedOrganizationInput?.props.value).toBe("acme");
+
+ await act(async () => {
+ resolveSso?.();
+ await Promise.resolve();
+ });
+ });
+
+ it("does not request an email code for an invalid email address", async () => {
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie");
+ });
+ expect(emailButton.props.accessibilityHint).toBe(
+ "Enter a valid email address to continue",
+ );
+ const [invalidEmailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ expect(invalidEmailButton?.props.accessibilityValue).toEqual({
+ text: "Email address is not valid",
+ });
+ expect(emailButton.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: true,
+ });
+ await act(async () => {
+ await emailButton.props.onPress();
+ });
+
+ expect(authFns.requestEmailCode).not.toHaveBeenCalled();
+ });
+
+ it("locks the email field while requesting a verification code", async () => {
+ let resolveRequest: (() => void) | null = null;
+ authFns.requestEmailCode.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveRequest = resolve;
+ }),
+ );
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie@cap.so");
+ });
+ await act(async () => {
+ void emailButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [loadingEmailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [loadingEmailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ expect(loadingEmailInput?.props.editable).toBe(false);
+ expect(loadingEmailInput?.props.accessibilityState).toEqual({
+ disabled: true,
+ });
+ expect(loadingEmailInput?.props.accessibilityValue).toEqual({
+ text: "Sending verification code",
+ });
+ expect(loadingEmailButton?.props.accessibilityHint).toBe(
+ "Sending verification code",
+ );
+ expect(loadingEmailButton?.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(loadingEmailButton?.props.accessibilityValue).toEqual({
+ text: "Sending verification code",
+ });
+ expect(getTextNodes(renderer.toJSON())).toContain("Login with email");
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Sending...");
+
+ await act(async () => {
+ emailInput.props.onChangeText("changed@cap.so");
+ });
+
+ const [unchangedEmailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ expect(unchangedEmailInput?.props.value).toBe("richie@cap.so");
+
+ await act(async () => {
+ resolveRequest?.();
+ await Promise.resolve();
+ });
+ });
+
+ it("locks browser links while requesting a verification code", async () => {
+ let resolveRequest: (() => void) | null = null;
+ authFns.requestEmailCode.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveRequest = resolve;
+ }),
+ );
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie@cap.so");
+ });
+ await act(async () => {
+ void emailButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ openBrowserAsync.mockClear();
+ const [signupLink, termsLink, privacyLink] = renderer.root.findAllByProps({
+ accessibilityRole: "link",
+ });
+ if (!signupLink || !termsLink || !privacyLink) {
+ throw new Error("Browser links were not rendered");
+ }
+
+ expect(signupLink.props.accessibilityState).toEqual({ disabled: true });
+ expect(signupLink.props.accessibilityHint).toBe("Sign in is in progress");
+ expect(signupLink.props.accessibilityValue).toEqual({
+ text: "Sending verification code",
+ });
+ expect(termsLink.props.accessibilityState).toEqual({ disabled: true });
+ expect(termsLink.props.accessibilityHint).toBe("Sign in is in progress");
+ expect(termsLink.props.accessibilityValue).toEqual({
+ text: "Sending verification code",
+ });
+ expect(privacyLink.props.accessibilityState).toEqual({ disabled: true });
+ expect(privacyLink.props.accessibilityHint).toBe("Sign in is in progress");
+ expect(privacyLink.props.accessibilityValue).toEqual({
+ text: "Sending verification code",
+ });
+
+ await act(async () => {
+ signupLink.props.onPress();
+ termsLink.props.onPress();
+ privacyLink.props.onPress();
+ });
+
+ expect(openBrowserAsync).not.toHaveBeenCalled();
+
+ await act(async () => {
+ resolveRequest?.();
+ await Promise.resolve();
+ });
+ });
+
+ it("deduplicates sign-in actions while a provider request is pending", async () => {
+ let resolveGoogle: (() => void) | null = null;
+ authFns.signInWithGoogle.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveGoogle = resolve;
+ }),
+ );
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ if (!emailInput) throw new Error("Email input was not rendered");
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie@cap.so");
+ });
+
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ const [googleButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with Google",
+ });
+ const [ssoButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with SAML SSO",
+ });
+ const [signupLink] = renderer.root.findAllByProps({
+ accessibilityHint: "Opens sign up in a browser sheet",
+ });
+ const [termsLink] = renderer.root.findAllByProps({
+ accessibilityHint: "Opens Terms of Service in a browser sheet",
+ });
+ const [privacyLink] = renderer.root.findAllByProps({
+ accessibilityHint: "Opens Privacy Policy in a browser sheet",
+ });
+ if (
+ !emailButton ||
+ !googleButton ||
+ !ssoButton ||
+ !signupLink ||
+ !termsLink ||
+ !privacyLink
+ ) {
+ throw new Error("Sign-in actions were not rendered");
+ }
+
+ await act(async () => {
+ void googleButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [loadingEmailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ const [loadingGoogleButton] = renderer.root.findAll(
+ (node) =>
+ node.props.accessibilityLabel === "Login with Google" &&
+ node.props.accessibilityHint === "Google sign in is starting",
+ );
+ const [loadingSsoButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with SAML SSO",
+ });
+ expect(loadingEmailButton?.props.disabled).toBe(true);
+ expect(loadingEmailButton?.props.accessibilityHint).toBe(
+ "Sign in is in progress",
+ );
+ expect(loadingEmailButton?.props.accessibilityValue).toEqual({
+ text: "Starting Google sign in",
+ });
+ expect(loadingGoogleButton?.props.accessibilityHint).toBe(
+ "Google sign in is starting",
+ );
+ expect(loadingGoogleButton?.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(loadingGoogleButton?.props.accessibilityValue).toEqual({
+ text: "Starting Google sign in",
+ });
+ expect(getTextNodes(renderer.toJSON())).toContain("Login with Google");
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Starting Google...");
+ expect(loadingSsoButton?.props.disabled).toBe(true);
+ expect(loadingSsoButton?.props.accessibilityHint).toBe(
+ "Sign in is in progress",
+ );
+ expect(loadingSsoButton?.props.accessibilityValue).toEqual({
+ text: "Starting Google sign in",
+ });
+
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ openBrowserAsync.mockClear();
+
+ await act(async () => {
+ googleButton.props.onPress();
+ emailButton.props.onPress();
+ ssoButton.props.onPress();
+ signupLink.props.onPress();
+ termsLink.props.onPress();
+ privacyLink.props.onPress();
+ await Promise.resolve();
+ });
+
+ expect(authFns.signInWithGoogle).toHaveBeenCalledTimes(1);
+ expect(authFns.requestEmailCode).not.toHaveBeenCalled();
+ expect(openBrowserAsync).not.toHaveBeenCalled();
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Continue with SSO");
+
+ await act(async () => {
+ resolveGoogle?.();
+ await Promise.resolve();
+ });
+ });
+
+ it("marks a failed Google sign-in as retryable", async () => {
+ authFns.signInWithGoogle.mockRejectedValueOnce(
+ new Error("Google unavailable"),
+ );
+ const renderer = await renderPanel();
+ const [googleButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with Google",
+ });
+ if (!googleButton) throw new Error("Google button was not rendered");
+
+ await act(async () => {
+ await googleButton.props.onPress();
+ });
+
+ expect(getTextNodes(renderer.toJSON())).toContain("Google unavailable");
+ const [retryGoogleButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Retry Google sign in",
+ });
+ const [errorAlert] = renderer.root.findAllByProps({
+ accessibilityRole: "alert",
+ });
+ expect(retryGoogleButton?.props.accessibilityHint).toBe(
+ "Google unavailable",
+ );
+ expect(retryGoogleButton?.props.accessibilityValue).toEqual({
+ text: "Google unavailable",
+ });
+ expect(errorAlert?.props.accessibilityLabel).toBe(
+ "Sign-in error: Google unavailable",
+ );
+
+ await act(async () => {
+ await retryGoogleButton?.props.onPress();
+ });
+
+ expect(authFns.signInWithGoogle).toHaveBeenCalledTimes(2);
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Google unavailable");
+ expect(
+ renderer.root.findAllByProps({
+ accessibilityLabel: "Retry Google sign in",
+ }),
+ ).toHaveLength(0);
+ });
+
+ it("marks a failed SSO sign-in on the organization step", async () => {
+ authFns.signInWithSso.mockRejectedValueOnce(new Error("SSO unavailable"));
+ const renderer = await renderPanel();
+ const [ssoButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with SAML SSO",
+ });
+ if (!ssoButton) throw new Error("SSO button was not rendered");
+
+ await act(async () => {
+ ssoButton.props.onPress();
+ });
+
+ const [organizationInput] = renderer.root.findAllByProps({
+ accessibilityLabel: "Organization ID",
+ });
+ if (!organizationInput)
+ throw new Error("Organization ID input was not rendered");
+ await act(async () => {
+ organizationInput.props.onChangeText("acme");
+ });
+
+ const [continueButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Continue with SSO",
+ });
+ if (!continueButton)
+ throw new Error("SSO continue button was not rendered");
+ await act(async () => {
+ await continueButton.props.onPress();
+ });
+
+ expect(getTextNodes(renderer.toJSON())).toContain("SSO unavailable");
+ const [organizationInputAfterError] = renderer.root.findAllByProps({
+ accessibilityLabel: "Organization ID",
+ });
+ const [retrySsoButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Retry SAML SSO sign in",
+ });
+ const [errorAlert] = renderer.root.findAllByProps({
+ accessibilityRole: "alert",
+ });
+ expect(organizationInputAfterError?.props.accessibilityHint).toBe(
+ "SSO unavailable",
+ );
+ expect(retrySsoButton?.props.accessibilityHint).toBe("SSO unavailable");
+ expect(retrySsoButton?.props.accessibilityValue).toEqual({
+ text: "SSO unavailable",
+ });
+ expect(errorAlert?.props.accessibilityLabel).toBe(
+ "Sign-in error: SSO unavailable",
+ );
+ expect(
+ hasStyle(renderer.toJSON(), {
+ borderColor: "#f4a9aa",
+ }),
+ ).toBe(true);
+
+ await act(async () => {
+ await retrySsoButton?.props.onPress();
+ });
+
+ expect(authFns.signInWithSso).toHaveBeenCalledTimes(2);
+ expect(getTextNodes(renderer.toJSON())).not.toContain("SSO unavailable");
+ expect(
+ renderer.root.findAllByProps({
+ accessibilityLabel: "Retry SAML SSO sign in",
+ }),
+ ).toHaveLength(0);
+ });
+
+ it("shows the right error when an email is not allowed", async () => {
+ const { MobileApiError } = await import("@/api/mobile");
+ authFns.requestEmailCode.mockRejectedValueOnce(
+ new MobileApiError("Forbidden", 403, null),
+ );
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("blocked@example.com");
+ });
+ await act(async () => {
+ await emailButton.props.onPress();
+ });
+
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "This email cannot be used to sign in to Cap.",
+ );
+ const [emailInputAfterError] = renderer.root.findAllByProps({
+ accessibilityLabel: "Email address",
+ });
+ const [errorAlert] = renderer.root.findAllByProps({
+ accessibilityRole: "alert",
+ });
+ expect(emailInputAfterError?.props.accessibilityHint).toBe(
+ "This email cannot be used to sign in to Cap.",
+ );
+ const [emailButtonAfterError] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ expect(emailButtonAfterError?.props.accessibilityValue).toEqual({
+ text: "This email cannot be used to sign in to Cap.",
+ });
+ expect(errorAlert?.props.accessibilityLabel).toBe(
+ "Sign-in error: This email cannot be used to sign in to Cap.",
+ );
+ expect(errorAlert?.props.accessibilityLiveRegion).toBe("polite");
+ expect(
+ hasStyle(renderer.toJSON(), {
+ borderColor: "#f4a9aa",
+ }),
+ ).toBe(true);
+ expect(
+ hasStyle(renderer.toJSON(), {
+ backgroundColor: "#fffcfc",
+ borderColor: "#fdbdbe",
+ }),
+ ).toBe(true);
+ });
+
+ it("switches to a web-like verification code step after requesting email", async () => {
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie@cap.so");
+ });
+ await act(async () => {
+ await emailButton.props.onPress();
+ });
+
+ const tree = renderer.toJSON();
+ const text = getTextNodes(tree);
+
+ expect(authFns.requestEmailCode).toHaveBeenCalledWith("richie@cap.so");
+ expect(text).toContain("Back");
+ expect(text).toContain("Enter verification code");
+ expect(text).toContain("We sent a 6-digit code to richie@cap.so");
+ expect(text).toContain("Verify Code");
+ expect(text).toContain("Resend in 30s");
+ expect(text).toContain("Terms of Service");
+ expect(hasProp(tree, "accessibilityLabel", "Verification code")).toBe(true);
+ const [codeTarget] = renderer.root.findAllByProps({
+ accessibilityLabel: "Verification code",
+ });
+ const [verifyButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Verify Code",
+ });
+ const [resendButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Resend in 30s",
+ });
+ const [codeInput] = renderer.root.findAllByProps({
+ textContentType: "oneTimeCode",
+ });
+ if (!codeInput) throw new Error("One-time code input was not rendered");
+ expect(codeTarget?.props.accessibilityValue).toEqual({
+ text: "0 of 6 digits entered",
+ });
+ expect(verifyButton?.props.accessibilityValue).toEqual({
+ text: "0 of 6 digits entered",
+ });
+ expect(resolveStyle(verifyButton?.props.style)).toMatchObject({
+ backgroundColor: "#d9d9d9",
+ borderColor: "#d9d9d9",
+ });
+ expect(resendButton?.props.accessibilityValue).toEqual({
+ text: "Wait 30 seconds",
+ });
+ expect(hasStyle(tree, { gap: 20, paddingTop: 2 })).toBe(true);
+ expect(
+ hasProp(tree, "accessibilityHint", "Tap to enter the 6-digit code"),
+ ).toBe(true);
+ expect(
+ hasProp(tree, "accessibilityHint", "Enter the 6-digit code to continue"),
+ ).toBe(true);
+ expect(hasProp(tree, "accessibilityHint", "Returns to email sign in")).toBe(
+ true,
+ );
+ expect(hasProp(tree, "accessibilityElementsHidden", true)).toBe(true);
+ expect(hasProp(tree, "accessible", false)).toBe(true);
+ expect(
+ hasProp(tree, "importantForAccessibility", "no-hide-descendants"),
+ ).toBe(true);
+ expect(hasProp(tree, "selectionColor", "#0090ff")).toBe(true);
+ await act(async () => {
+ codeInput.props.onFocus();
+ });
+ expect(
+ hasStyle(renderer.toJSON(), {
+ backgroundColor: "#f9f9f9",
+ borderColor: "#0090ff",
+ shadowOpacity: 0.12,
+ }),
+ ).toBe(true);
+ await act(async () => {
+ codeInput.props.onBlur();
+ });
+ expect(
+ hasStyle(renderer.toJSON(), {
+ backgroundColor: "#f9f9f9",
+ borderColor: "#0090ff",
+ shadowOpacity: 0.12,
+ }),
+ ).toBe(false);
+ expect(text).not.toContain("Login with Google");
+ });
+
+ it("verifies an autofilled one-time code when all six digits are entered", async () => {
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie@cap.so");
+ });
+ await act(async () => {
+ await emailButton.props.onPress();
+ });
+
+ const [codeInput] = renderer.root.findAllByProps({
+ textContentType: "oneTimeCode",
+ });
+ if (!codeInput) throw new Error("One-time code input was not rendered");
+
+ await act(async () => {
+ codeInput.props.onChangeText("123-456");
+ await Promise.resolve();
+ });
+
+ expect(authFns.verifyEmailCode).toHaveBeenCalledWith(
+ "richie@cap.so",
+ "123456",
+ );
+ });
+
+ it("marks invalid verification codes on the visible code target", async () => {
+ const { MobileApiError } = await import("@/api/mobile");
+ authFns.verifyEmailCode.mockRejectedValueOnce(
+ new MobileApiError("Forbidden", 403, null),
+ );
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie@cap.so");
+ });
+ await act(async () => {
+ await emailButton.props.onPress();
+ });
+
+ const [codeInput] = renderer.root.findAllByProps({
+ textContentType: "oneTimeCode",
+ });
+ if (!codeInput) throw new Error("One-time code input was not rendered");
+
+ await act(async () => {
+ codeInput.props.onChangeText("123456");
+ await Promise.resolve();
+ });
+
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "That code is invalid or expired.",
+ );
+ const [codeTarget] = renderer.root.findAllByProps({
+ accessibilityLabel: "Verification code",
+ });
+ const [errorAlert] = renderer.root.findAllByProps({
+ accessibilityRole: "alert",
+ });
+ expect(codeTarget?.props.accessibilityHint).toBe(
+ "That code is invalid or expired.",
+ );
+ expect(codeTarget?.props.accessibilityValue).toEqual({
+ text: "0 of 6 digits entered",
+ });
+ expect(errorAlert?.props.accessibilityLabel).toBe(
+ "Sign-in error: That code is invalid or expired.",
+ );
+ expect(
+ hasStyle(renderer.toJSON(), {
+ backgroundColor: "#fffcfc",
+ borderColor: "#f4a9aa",
+ }),
+ ).toBe(true);
+ });
+
+ it("locks the visible code entry target while verifying", async () => {
+ let resolveVerify: (() => void) | null = null;
+ authFns.verifyEmailCode.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveVerify = resolve;
+ }),
+ );
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie@cap.so");
+ });
+ await act(async () => {
+ await emailButton.props.onPress();
+ });
+
+ const [codeInput] = renderer.root.findAllByProps({
+ textContentType: "oneTimeCode",
+ });
+ if (!codeInput) throw new Error("One-time code input was not rendered");
+
+ await act(async () => {
+ codeInput.props.onChangeText("123456");
+ await Promise.resolve();
+ });
+
+ const [codeTarget] = renderer.root.findAllByProps({
+ accessibilityLabel: "Verification code",
+ });
+ const [loadingBackButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Back",
+ });
+ const [loadingCodeInput] = renderer.root.findAllByProps({
+ textContentType: "oneTimeCode",
+ });
+ const [loadingVerifyButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Verify Code",
+ });
+ expect(codeTarget?.props.disabled).toBe(true);
+ expect(codeTarget?.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(codeTarget?.props.accessibilityHint).toBe("Verifying code");
+ expect(codeTarget?.props.accessibilityValue).toEqual({
+ text: "Verifying code",
+ });
+ expect(loadingBackButton?.props.accessibilityHint).toBe(
+ "Sign in is in progress",
+ );
+ expect(loadingBackButton?.props.accessibilityValue).toEqual({
+ text: "Verifying code",
+ });
+ expect(loadingCodeInput?.props.editable).toBe(false);
+ expect(loadingVerifyButton?.props.accessibilityHint).toBe("Verifying code");
+ expect(loadingVerifyButton?.props.accessibilityValue).toEqual({
+ text: "Verifying code",
+ });
+ expect(loadingVerifyButton?.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Verifying...");
+
+ await act(async () => {
+ codeInput.props.onChangeText("654321");
+ await Promise.resolve();
+ });
+
+ const [unchangedCodeInput] = renderer.root.findAllByProps({
+ textContentType: "oneTimeCode",
+ });
+ expect(unchangedCodeInput?.props.value).toBe("123456");
+ expect(authFns.verifyEmailCode).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ resolveVerify?.();
+ await Promise.resolve();
+ });
+ });
+
+ it("prevents repeated email code requests during the resend cooldown", async () => {
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("richie@cap.so");
+ });
+ await act(async () => {
+ await emailButton.props.onPress();
+ });
+
+ const [resendButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Resend in 30s",
+ });
+ if (!resendButton) throw new Error("Resend control was not rendered");
+
+ expect(resendButton.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: true,
+ });
+ expect(resendButton.props.accessibilityHint).toBe(
+ "Wait 30 seconds before requesting a new code",
+ );
+ expect(resendButton.props.accessibilityValue).toEqual({
+ text: "Wait 30 seconds",
+ });
+ expect(resendButton.props.hitSlop).toBe(6);
+ expect(
+ hasStyle(renderer.toJSON(), {
+ color: "#8d8d8d",
+ textDecorationLine: "none",
+ }),
+ ).toBe(true);
+
+ await act(async () => {
+ await resendButton.props.onPress();
+ });
+
+ expect(authFns.requestEmailCode).toHaveBeenCalledTimes(1);
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "Please wait 30 seconds before requesting a new code.",
+ );
+ });
+
+ it("allows a corrected email to request a code without waiting for the previous cooldown", async () => {
+ const renderer = await renderPanel();
+ const [emailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [emailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!emailInput || !emailButton) {
+ throw new Error("Email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ emailInput.props.onChangeText("wrong@cap.so");
+ });
+ await act(async () => {
+ await emailButton.props.onPress();
+ });
+
+ const [backButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Back",
+ });
+ if (!backButton) throw new Error("Back button was not rendered");
+
+ await act(async () => {
+ backButton.props.onPress();
+ });
+
+ const [correctedEmailInput] = renderer.root.findAllByProps({
+ placeholder: "tim@apple.com",
+ });
+ const [correctedEmailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ if (!correctedEmailInput || !correctedEmailButton) {
+ throw new Error("Corrected email sign in controls were not rendered");
+ }
+
+ await act(async () => {
+ correctedEmailInput.props.onChangeText("right@cap.so");
+ });
+
+ const [readyEmailButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Login with email",
+ });
+ expect(readyEmailButton?.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: false,
+ });
+
+ await act(async () => {
+ await readyEmailButton?.props.onPress();
+ });
+
+ expect(authFns.requestEmailCode).toHaveBeenNthCalledWith(1, "wrong@cap.so");
+ expect(authFns.requestEmailCode).toHaveBeenNthCalledWith(2, "right@cap.so");
+ });
+});
diff --git a/apps/mobile/src/auth/SignInPanel.tsx b/apps/mobile/src/auth/SignInPanel.tsx
new file mode 100644
index 00000000000..27f6f3eae0a
--- /dev/null
+++ b/apps/mobile/src/auth/SignInPanel.tsx
@@ -0,0 +1,1053 @@
+import { SymbolView } from "expo-symbols";
+import * as WebBrowser from "expo-web-browser";
+import { useEffect, useRef, useState } from "react";
+import { Pressable, StyleSheet, Text, TextInput, View } from "react-native";
+import Svg, { Path } from "react-native-svg";
+import { MobileApiError } from "@/api/mobile";
+import { ActionButton } from "@/components/ActionButton";
+import { CapLogoBadge } from "@/components/CapLogoBadge";
+import { GlassSurface } from "@/components/GlassSurface";
+import { colors, fonts, radius, squircle } from "@/theme";
+import { apiBaseUrl, useAuth } from "./AuthContext";
+
+type SignInPanelProps = {
+ title?: string;
+ subtitle?: string;
+};
+
+const codePattern = /^\d{6}$/;
+const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+const emailCodeCooldownMs = 30_000;
+const codeSlots = ["code-0", "code-1", "code-2", "code-3", "code-4", "code-5"];
+type FocusedInput = "code" | "email" | "sso" | null;
+type LoadingKind = "email" | "code" | "google" | "sso";
+type SignInError = {
+ message: string;
+ source: LoadingKind | "resend";
+};
+
+const getEmailRequestErrorMessage = (error: unknown) => {
+ if (error instanceof MobileApiError) {
+ if (error.status === 400) return "Enter a valid email address.";
+ if (error.status === 403) {
+ return "This email cannot be used to sign in to Cap.";
+ }
+ }
+ return error instanceof Error
+ ? error.message
+ : "Unable to send a code. Try again.";
+};
+
+const getCodeVerificationErrorMessage = (error: unknown) => {
+ if (error instanceof MobileApiError) {
+ if (error.status === 400) return "Enter a valid email and 6-digit code.";
+ if (error.status === 403) return "That code is invalid or expired.";
+ }
+ return error instanceof Error ? error.message : "Unable to verify that code.";
+};
+
+const getProviderErrorMessage = (error: unknown, fallback: string) => {
+ return error instanceof Error ? error.message : fallback;
+};
+
+const openWebPath = (path: string) => {
+ void WebBrowser.openBrowserAsync(new URL(path, apiBaseUrl).toString());
+};
+
+function GoogleMark() {
+ return (
+
+ );
+}
+
+export function SignInPanel({
+ title = "Sign in to Cap",
+ subtitle = "Your videos, organized and ready to share.",
+}: SignInPanelProps) {
+ const auth = useAuth();
+ const codeInputRef = useRef(null);
+ const loadingRef = useRef(false);
+ const [email, setEmail] = useState("");
+ const [code, setCode] = useState("");
+ const [organizationId, setOrganizationId] = useState("");
+ const [codeSent, setCodeSent] = useState(false);
+ const [lastCodeRequestedAt, setLastCodeRequestedAt] = useState(
+ null,
+ );
+ const [lastCodeRequestedEmail, setLastCodeRequestedEmail] = useState<
+ string | null
+ >(null);
+ const [nowMs, setNowMs] = useState(() => Date.now());
+ const [showSso, setShowSso] = useState(false);
+ const [focusedInput, setFocusedInput] = useState(null);
+ const [loading, setLoading] = useState(null);
+ const [error, setError] = useState(null);
+
+ const normalizedEmail = email.trim().toLowerCase();
+ const normalizedOrganizationId = organizationId.trim();
+ const cooldownEndsAt =
+ lastCodeRequestedAt !== null && lastCodeRequestedEmail === normalizedEmail
+ ? lastCodeRequestedAt + emailCodeCooldownMs
+ : null;
+ const cooldownRemainingMs =
+ cooldownEndsAt !== null ? Math.max(0, cooldownEndsAt - nowMs) : 0;
+ const cooldownRemainingSeconds = Math.ceil(cooldownRemainingMs / 1000);
+ const isCodeRequestCoolingDown = cooldownRemainingSeconds > 0;
+ const isEmailReady = emailPattern.test(normalizedEmail);
+ const isCodeReady = codePattern.test(code);
+ const isSsoReady = normalizedOrganizationId.length > 0;
+ const canRequestCode =
+ isEmailReady && loading === null && !isCodeRequestCoolingDown;
+ const canVerifyCode = isCodeReady && loading === null;
+ const canStartSso = isSsoReady && loading === null;
+ const isCodeStep = codeSent && !showSso;
+ const showBackButton = showSso || isCodeStep;
+ const showGoogle = auth.authConfig.googleAuthAvailable;
+ const showSaml = auth.authConfig.workosAuthAvailable;
+ const showProviderOptions = showGoogle || showSaml;
+ const errorMessage = error?.message ?? null;
+ const emailInputHasError =
+ error?.source === "email" && !showSso && !isCodeStep;
+ const ssoInputHasError = error?.source === "sso" && showSso;
+ const codeEntryHasError = error?.source === "code" && isCodeStep;
+ const googleActionHasError =
+ error?.source === "google" && !showSso && !isCodeStep;
+ const ssoActionHasError = error?.source === "sso" && showSso;
+ const backDisabled = loading !== null;
+ const codeEntryDisabled = loading !== null;
+ const activeCodeSlotIndex = Math.min(code.length, codeSlots.length - 1);
+ const linkDisabled = loading !== null;
+ const resendDisabled = loading !== null || isCodeRequestCoolingDown;
+ const headerTitle = isCodeStep ? "Enter verification code" : title;
+ const headerSubtitle = isCodeStep
+ ? `We sent a 6-digit code to ${normalizedEmail}`
+ : subtitle;
+ const resendLabel = isCodeRequestCoolingDown
+ ? `Resend in ${cooldownRemainingSeconds}s`
+ : "Didn't receive the code? Resend";
+ const emailButtonLabel = "Login with email";
+ const verifyButtonLabel = "Verify Code";
+ const googleButtonLabel = googleActionHasError
+ ? "Retry Google"
+ : "Login with Google";
+ const ssoContinueButtonLabel = ssoActionHasError
+ ? "Retry SSO"
+ : "Continue with SSO";
+ const googleButtonAccessibilityLabel = googleActionHasError
+ ? "Retry Google sign in"
+ : undefined;
+ const ssoContinueButtonAccessibilityLabel = ssoActionHasError
+ ? "Retry SAML SSO sign in"
+ : undefined;
+ const activeSignInAccessibilityText =
+ loading === "email"
+ ? "Sending verification code"
+ : loading === "code"
+ ? "Verifying code"
+ : loading === "google"
+ ? "Starting Google sign in"
+ : loading === "sso"
+ ? "Starting SAML SSO sign in"
+ : null;
+ const activeSignInAccessibilityValue = activeSignInAccessibilityText
+ ? { text: activeSignInAccessibilityText }
+ : undefined;
+ const emailButtonAccessibilityValue =
+ loading === "email"
+ ? activeSignInAccessibilityValue
+ : emailInputHasError && errorMessage
+ ? { text: errorMessage }
+ : normalizedEmail.length > 0 && !isEmailReady
+ ? { text: "Email address is not valid" }
+ : loading !== null
+ ? activeSignInAccessibilityValue
+ : undefined;
+ const verifyButtonAccessibilityValue =
+ loading === "code"
+ ? activeSignInAccessibilityValue
+ : isCodeStep
+ ? { text: `${code.length} of 6 digits entered` }
+ : undefined;
+ const googleButtonAccessibilityValue =
+ loading === "google"
+ ? activeSignInAccessibilityValue
+ : googleActionHasError && errorMessage
+ ? { text: errorMessage }
+ : loading !== null
+ ? activeSignInAccessibilityValue
+ : undefined;
+ const samlButtonAccessibilityValue =
+ loading !== null ? activeSignInAccessibilityValue : undefined;
+ const ssoContinueButtonAccessibilityValue =
+ loading === "sso"
+ ? activeSignInAccessibilityValue
+ : ssoActionHasError && errorMessage
+ ? { text: errorMessage }
+ : normalizedOrganizationId.length > 0
+ ? undefined
+ : { text: "Organization ID required" };
+ const resendAccessibilityLabel =
+ loading === "email" ? "Didn't receive the code? Resend" : resendLabel;
+ const resendAccessibilityValue =
+ loading === "email"
+ ? activeSignInAccessibilityValue
+ : loading !== null
+ ? activeSignInAccessibilityValue
+ : isCodeRequestCoolingDown
+ ? { text: `Wait ${cooldownRemainingSeconds} seconds` }
+ : undefined;
+ const codeEntryAccessibilityValue =
+ loading === "code"
+ ? activeSignInAccessibilityValue
+ : { text: `${code.length} of 6 digits entered` };
+ const linkAccessibilityValue =
+ loading !== null ? activeSignInAccessibilityValue : undefined;
+ const emailInputAccessibilityValue =
+ loading !== null ? activeSignInAccessibilityValue : undefined;
+ const ssoInputAccessibilityValue =
+ loading !== null
+ ? activeSignInAccessibilityValue
+ : ssoInputHasError && errorMessage
+ ? { text: errorMessage }
+ : undefined;
+ const backButtonAccessibilityValue =
+ loading !== null ? activeSignInAccessibilityValue : undefined;
+ const resendHint =
+ loading !== null
+ ? "Sign in is in progress"
+ : isCodeRequestCoolingDown
+ ? `Wait ${cooldownRemainingSeconds} seconds before requesting a new code`
+ : "Requests a new verification code";
+ const emailButtonHint =
+ loading === "email"
+ ? "Sending verification code"
+ : loading !== null
+ ? "Sign in is in progress"
+ : !isEmailReady
+ ? "Enter a valid email address to continue"
+ : "Sends a verification code to this email";
+ const verifyButtonHint =
+ loading === "code"
+ ? "Verifying code"
+ : loading !== null
+ ? "Sign in is in progress"
+ : !isCodeReady
+ ? "Enter the 6-digit code to continue"
+ : "Verifies the 6-digit code";
+ const googleButtonHint =
+ loading === "google"
+ ? "Google sign in is starting"
+ : loading !== null
+ ? "Sign in is in progress"
+ : googleActionHasError && errorMessage
+ ? errorMessage
+ : "Starts Google sign in";
+ const samlButtonHint =
+ loading !== null
+ ? "Sign in is in progress"
+ : "Shows the SSO organization step";
+ const ssoButtonHint =
+ loading === "sso"
+ ? "SAML SSO sign in is starting"
+ : loading !== null
+ ? "Sign in is in progress"
+ : ssoActionHasError && errorMessage
+ ? errorMessage
+ : !isSsoReady
+ ? "Enter your organization ID to continue"
+ : "Starts SAML SSO for this organization";
+ const emailInputHint =
+ emailInputHasError && errorMessage
+ ? errorMessage
+ : "Enter your email to request a verification code";
+ const ssoInputHint =
+ ssoInputHasError && errorMessage
+ ? errorMessage
+ : "Enter your organization ID to continue with SSO";
+ const codeEntryHint =
+ codeEntryHasError && errorMessage
+ ? errorMessage
+ : loading === "code"
+ ? "Verifying code"
+ : loading !== null
+ ? "Sign in is in progress"
+ : "Tap to enter the 6-digit code";
+ const signupLinkHint =
+ loading !== null
+ ? "Sign in is in progress"
+ : "Opens sign up in a browser sheet";
+ const termsLinkHint =
+ loading !== null
+ ? "Sign in is in progress"
+ : "Opens Terms of Service in a browser sheet";
+ const privacyLinkHint =
+ loading !== null
+ ? "Sign in is in progress"
+ : "Opens Privacy Policy in a browser sheet";
+ const backButtonHint =
+ loading !== null
+ ? "Sign in is in progress"
+ : isCodeStep
+ ? "Returns to email sign in"
+ : "Returns to sign in options";
+
+ const beginLoading = (nextLoading: LoadingKind) => {
+ if (loadingRef.current || loading !== null) return false;
+ loadingRef.current = true;
+ setLoading(nextLoading);
+ return true;
+ };
+
+ const endLoading = () => {
+ loadingRef.current = false;
+ setLoading(null);
+ };
+
+ const isAuthBusy = () => loadingRef.current || loading !== null;
+
+ useEffect(() => {
+ if (!isCodeRequestCoolingDown) return;
+ const interval = setInterval(() => setNowMs(Date.now()), 1000);
+ return () => clearInterval(interval);
+ }, [isCodeRequestCoolingDown]);
+
+ const goBack = () => {
+ if (isAuthBusy()) return;
+ if (showSso) setShowSso(false);
+ if (isCodeStep) {
+ setCodeSent(false);
+ setCode("");
+ }
+ setFocusedInput(null);
+ setError(null);
+ };
+
+ const updateOrganizationId = (value: string) => {
+ if (isAuthBusy()) return;
+ setOrganizationId(value.trim());
+ setError(null);
+ };
+
+ const updateEmail = (value: string) => {
+ if (isAuthBusy()) return;
+ setEmail(value.toLowerCase());
+ setCodeSent(false);
+ setCode("");
+ setError(null);
+ };
+
+ const requestCode = async () => {
+ if (!emailPattern.test(normalizedEmail)) return;
+ if (isCodeRequestCoolingDown) {
+ setError({
+ message: `Please wait ${cooldownRemainingSeconds} seconds before requesting a new code.`,
+ source: "resend",
+ });
+ return;
+ }
+ if (!beginLoading("email")) return;
+ setError(null);
+ try {
+ await auth.requestEmailCode(normalizedEmail);
+ const requestedAt = Date.now();
+ setEmail(normalizedEmail);
+ setCode("");
+ setCodeSent(true);
+ setLastCodeRequestedAt(requestedAt);
+ setLastCodeRequestedEmail(normalizedEmail);
+ setNowMs(requestedAt);
+ } catch (requestError) {
+ setError({
+ message: getEmailRequestErrorMessage(requestError),
+ source: "email",
+ });
+ } finally {
+ endLoading();
+ }
+ };
+
+ const verifyCode = async (codeToVerify = code) => {
+ if (!codePattern.test(codeToVerify)) return;
+ if (!beginLoading("code")) return;
+ setError(null);
+ try {
+ await auth.verifyEmailCode(normalizedEmail, codeToVerify);
+ } catch (verifyError) {
+ setError({
+ message: getCodeVerificationErrorMessage(verifyError),
+ source: "code",
+ });
+ setCode("");
+ } finally {
+ endLoading();
+ }
+ };
+
+ const updateCode = (value: string) => {
+ if (isAuthBusy()) return;
+ const nextCode = value.replace(/\D/g, "").slice(0, 6);
+ setCode(nextCode);
+ setError(null);
+ if (codePattern.test(nextCode)) void verifyCode(nextCode);
+ };
+
+ const submitCode = () => {
+ void verifyCode();
+ };
+
+ const focusCodeInput = () => {
+ if (codeEntryDisabled) return;
+ setFocusedInput("code");
+ codeInputRef.current?.focus();
+ };
+
+ const openWebPathIfIdle = (path: string) => {
+ if (isAuthBusy()) return;
+ openWebPath(path);
+ };
+
+ const signInWithGoogle = async () => {
+ if (!beginLoading("google")) return;
+ setError(null);
+ try {
+ await auth.signInWithGoogle();
+ } catch (googleError) {
+ setError({
+ message: getProviderErrorMessage(
+ googleError,
+ "Unable to start Google sign in.",
+ ),
+ source: "google",
+ });
+ } finally {
+ endLoading();
+ }
+ };
+
+ const signInWithSso = async () => {
+ if (normalizedOrganizationId.length === 0) return;
+ if (!beginLoading("sso")) return;
+ setError(null);
+ try {
+ await auth.signInWithSso(normalizedOrganizationId);
+ } catch (ssoError) {
+ setError({
+ message: getProviderErrorMessage(
+ ssoError,
+ "Unable to start SSO sign in.",
+ ),
+ source: "sso",
+ });
+ } finally {
+ endLoading();
+ }
+ };
+
+ const showSsoStep = () => {
+ if (isAuthBusy()) return;
+ setShowSso(true);
+ setCodeSent(false);
+ setCode("");
+ setError(null);
+ };
+
+ return (
+
+
+ {showBackButton ? (
+ [
+ styles.backPill,
+ pressed && !backDisabled && styles.backPillPressed,
+ backDisabled && styles.backPillDisabled,
+ ]}
+ >
+
+
+ Back
+
+
+ ) : null}
+
+
+
+
+
+ {headerTitle}
+
+
+ {headerSubtitle}
+
+
+
+ {showSso ? (
+
+ setFocusedInput(null)}
+ onFocus={() => setFocusedInput("sso")}
+ onSubmitEditing={signInWithSso}
+ style={[
+ styles.input,
+ focusedInput === "sso" && styles.inputFocused,
+ ssoInputHasError && styles.inputError,
+ ]}
+ />
+
+
+ ) : isCodeStep ? (
+
+
+ {codeSlots.map((slot, index) => (
+
+ {code[index] ?? ""}
+
+ ))}
+ setFocusedInput(null)}
+ onFocus={() => setFocusedInput("code")}
+ onSubmitEditing={submitCode}
+ maxLength={6}
+ importantForAccessibility="no-hide-descendants"
+ selectionColor={colors.blue9}
+ textContentType="oneTimeCode"
+ style={styles.codeInput}
+ />
+
+
+
+ ) : (
+ <>
+ setFocusedInput(null)}
+ onFocus={() => setFocusedInput("email")}
+ onSubmitEditing={requestCode}
+ style={[
+ styles.input,
+ focusedInput === "email" && styles.inputFocused,
+ emailInputHasError && styles.inputError,
+ ]}
+ />
+
+ >
+ )}
+ {errorMessage ? (
+
+
+ {errorMessage}
+
+ ) : null}
+ {isCodeStep ? (
+
+
+
+ {resendLabel}
+
+
+
+ ) : null}
+ {showSso || isCodeStep ? null : (
+ <>
+
+ Don't have an account?{" "}
+ openWebPathIfIdle("signup")}
+ >
+ Sign up here
+
+
+ {showProviderOptions ? (
+ <>
+
+
+ OR
+
+
+ {showGoogle ? (
+ }
+ onPress={signInWithGoogle}
+ loading={loading === "google"}
+ disabled={loading !== null}
+ variant="gray"
+ size="md"
+ />
+ ) : null}
+ {showSaml ? (
+
+ ) : null}
+ >
+ ) : null}
+ >
+ )}
+
+ {isCodeStep
+ ? "By entering your email, you acknowledge that you have both read and agree to Cap's "
+ : "By typing your email and clicking continue, you acknowledge that you have both read and agree to Cap's "}
+ openWebPathIfIdle("terms")}
+ >
+ Terms of Service
+ {" "}
+ and{" "}
+ openWebPathIfIdle("privacy")}
+ >
+ Privacy Policy
+
+ .
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ shell: {
+ flex: 1,
+ justifyContent: "center",
+ paddingVertical: 22,
+ },
+ card: {
+ width: "100%",
+ maxWidth: 432,
+ alignSelf: "center",
+ borderRadius: radius.lg,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray5,
+ paddingHorizontal: 28,
+ paddingVertical: 28,
+ gap: 28,
+ ...squircle,
+ },
+ cardFallback: {
+ backgroundColor: colors.gray3,
+ },
+ backPill: {
+ position: "absolute",
+ left: 20,
+ top: 20,
+ zIndex: 2,
+ minHeight: 30,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ borderRadius: radius.full,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray4,
+ paddingHorizontal: 12,
+ backgroundColor: "transparent",
+ ...squircle,
+ },
+ backPillPressed: {
+ backgroundColor: colors.gray1,
+ },
+ backPillDisabled: {
+ opacity: 0.55,
+ },
+ backPillText: {
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ lineHeight: 17,
+ color: colors.gray12,
+ },
+ backPillTextDisabled: {
+ color: colors.gray9,
+ },
+ brandBlock: {
+ alignItems: "center",
+ },
+ header: {
+ alignItems: "center",
+ gap: 8,
+ },
+ title: {
+ fontFamily: fonts.medium,
+ fontSize: 24,
+ lineHeight: 30,
+ color: colors.gray12,
+ textAlign: "center",
+ },
+ codeTitle: {
+ fontSize: 20,
+ lineHeight: 26,
+ },
+ subtitle: {
+ fontFamily: fonts.regular,
+ fontSize: 16,
+ lineHeight: 22,
+ color: colors.gray10,
+ textAlign: "center",
+ },
+ codeSubtitle: {
+ fontSize: 14,
+ lineHeight: 20,
+ },
+ formStack: {
+ gap: 12,
+ },
+ input: {
+ minHeight: 44,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray4,
+ backgroundColor: colors.gray1,
+ paddingHorizontal: 14,
+ fontFamily: fonts.regular,
+ fontSize: 16,
+ color: colors.gray12,
+ ...squircle,
+ },
+ inputFocused: {
+ backgroundColor: colors.gray2,
+ borderWidth: 1,
+ borderColor: colors.gray5,
+ shadowColor: colors.gray12,
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 0.12,
+ shadowRadius: 1,
+ },
+ inputError: {
+ backgroundColor: colors.red1,
+ borderWidth: 1,
+ borderColor: colors.red7,
+ },
+ codeSection: {
+ gap: 20,
+ paddingTop: 2,
+ },
+ codeBoxes: {
+ flexDirection: "row",
+ gap: 8,
+ justifyContent: "space-between",
+ position: "relative",
+ },
+ codeBoxesDisabled: {
+ opacity: 0.68,
+ },
+ codeBox: {
+ flex: 1,
+ height: 52,
+ borderRadius: radius.sm,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray5,
+ backgroundColor: colors.gray1,
+ alignItems: "center",
+ justifyContent: "center",
+ ...squircle,
+ },
+ codeBoxActive: {
+ borderColor: colors.blue9,
+ },
+ codeBoxFocused: {
+ backgroundColor: colors.gray2,
+ borderWidth: 1,
+ shadowColor: colors.gray12,
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 0.12,
+ shadowRadius: 1,
+ },
+ codeBoxError: {
+ backgroundColor: colors.red1,
+ borderColor: colors.red7,
+ },
+ codeDigit: {
+ fontFamily: fonts.medium,
+ fontSize: 22,
+ lineHeight: 27,
+ color: colors.gray12,
+ textAlign: "center",
+ },
+ ssoStack: {
+ gap: 10,
+ },
+ codeInput: {
+ position: "absolute",
+ width: 1,
+ height: 1,
+ opacity: 0,
+ },
+ codeLinks: {
+ alignItems: "center",
+ marginTop: 2,
+ },
+ resendButton: {
+ minHeight: 30,
+ justifyContent: "center",
+ paddingHorizontal: 4,
+ },
+ resendText: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray10,
+ textDecorationLine: "underline",
+ },
+ resendTextDisabled: {
+ color: colors.gray9,
+ textDecorationLine: "none",
+ },
+ errorBanner: {
+ minHeight: 42,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.red6,
+ backgroundColor: colors.red1,
+ paddingHorizontal: 12,
+ paddingVertical: 10,
+ ...squircle,
+ },
+ dividerRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 10,
+ paddingVertical: 3,
+ },
+ divider: {
+ flex: 1,
+ height: StyleSheet.hairlineWidth,
+ backgroundColor: colors.gray5,
+ },
+ dividerText: {
+ fontFamily: fonts.medium,
+ fontSize: 12,
+ textTransform: "uppercase",
+ color: colors.gray9,
+ },
+ legalText: {
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ lineHeight: 18,
+ color: colors.gray9,
+ textAlign: "center",
+ },
+ legalLink: {
+ fontFamily: fonts.medium,
+ color: colors.gray12,
+ },
+ linkDisabled: {
+ opacity: 0.55,
+ },
+ signupText: {
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ lineHeight: 18,
+ color: colors.gray9,
+ textAlign: "center",
+ },
+ signupLink: {
+ fontFamily: fonts.medium,
+ color: colors.blue9,
+ },
+ errorText: {
+ flex: 1,
+ fontFamily: fonts.medium,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.red9,
+ },
+});
diff --git a/apps/mobile/src/auth/session.ts b/apps/mobile/src/auth/session.ts
new file mode 100644
index 00000000000..be8f9b87fe3
--- /dev/null
+++ b/apps/mobile/src/auth/session.ts
@@ -0,0 +1,20 @@
+export const parseAuthRedirect = (url: string) => {
+ const parsed = new URL(url);
+ const error = parsed.searchParams.get("error_description");
+ if (error) throw new Error(error);
+
+ const apiKey = parsed.searchParams.get("api_key");
+ const userId = parsed.searchParams.get("user_id");
+
+ if (!apiKey) return null;
+ return {
+ apiKey,
+ userId,
+ };
+};
+
+export const requireAuthRedirectSession = (url: string) => {
+ const session = parseAuthRedirect(url);
+ if (!session) throw new Error("Sign in did not return a mobile session.");
+ return session;
+};
diff --git a/apps/mobile/src/auth/signInDestination.test.ts b/apps/mobile/src/auth/signInDestination.test.ts
new file mode 100644
index 00000000000..8ca3f9b70e4
--- /dev/null
+++ b/apps/mobile/src/auth/signInDestination.test.ts
@@ -0,0 +1,12 @@
+import { describe, expect, it } from "vitest";
+import { signInTitleForSegments } from "./signInDestination";
+
+describe("signInTitleForSegments", () => {
+ it("uses contextual auth titles for deep-linked mobile surfaces", () => {
+ expect(signInTitleForSegments(["(tabs)", "upload"])).toBe(
+ "Sign in to import",
+ );
+ expect(signInTitleForSegments(["caps", "[id]"])).toBe("Sign in to view");
+ expect(signInTitleForSegments(["(tabs)"])).toBe("Sign in to Cap");
+ });
+});
diff --git a/apps/mobile/src/auth/signInDestination.ts b/apps/mobile/src/auth/signInDestination.ts
new file mode 100644
index 00000000000..033439d1463
--- /dev/null
+++ b/apps/mobile/src/auth/signInDestination.ts
@@ -0,0 +1,5 @@
+export const signInTitleForSegments = (segments: readonly string[]) => {
+ if (segments.includes("upload")) return "Sign in to import";
+ if (segments.includes("caps")) return "Sign in to view";
+ return "Sign in to Cap";
+};
diff --git a/apps/mobile/src/caps/CapSettingsSheet.test.tsx b/apps/mobile/src/caps/CapSettingsSheet.test.tsx
new file mode 100644
index 00000000000..535558bc5a7
--- /dev/null
+++ b/apps/mobile/src/caps/CapSettingsSheet.test.tsx
@@ -0,0 +1,319 @@
+import { Video } from "@cap/web-domain";
+import type { ReactElement, ReactNode } from "react";
+import { Switch } from "react-native";
+import TestRenderer, {
+ act,
+ type ReactTestInstance,
+ type ReactTestRenderer,
+ type ReactTestRendererJSON,
+} from "react-test-renderer";
+import { describe, expect, it, vi } from "vitest";
+import type { MobileCapSummary } from "@/api/mobile";
+import { CapSettingsSheet } from "./CapSettingsSheet";
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+type JsonNode = ReactTestRendererJSON | ReactTestRendererJSON[] | string | null;
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const renderComponent = async (
+ node: ReactElement,
+): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+const getTextNodes = (node: JsonNode): string[] => {
+ if (!node) return [];
+ if (typeof node === "string") return [node];
+ if (Array.isArray(node)) return node.flatMap(getTextNodes);
+ return node.children?.flatMap(getTextNodes) ?? [];
+};
+
+const getInstanceText = (node: ReactTestInstance): string[] =>
+ node.children.flatMap((child) =>
+ typeof child === "string" ? [child] : getInstanceText(child),
+ );
+
+const getNodeType = (node: ReactTestInstance) => String(node.type);
+const resolveStyle = (
+ style: unknown,
+ pressed = false,
+): Record => {
+ const resolved = typeof style === "function" ? style({ pressed }) : style;
+ const styles = Array.isArray(resolved) ? resolved : [resolved];
+ return Object.assign({}, ...styles.filter(Boolean));
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ Modal: createHost("Modal"),
+ Pressable: createHost("Pressable"),
+ ScrollView: createHost("ScrollView"),
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ hairlineWidth: 1,
+ },
+ Switch: createHost("Switch"),
+ Text: createHost("Text"),
+ View: createHost("View"),
+ };
+});
+
+vi.mock("expo-symbols", async () => {
+ const React = await import("react");
+ return {
+ SymbolView: (props: Record) =>
+ React.createElement("SymbolView", props),
+ };
+});
+
+vi.mock("@/components/GlassSurface", async () => {
+ const React = await import("react");
+ return {
+ GlassSurface: ({ children, ...props }: HostProps) =>
+ React.createElement("GlassSurface", props, children),
+ };
+});
+
+const cap: MobileCapSummary = {
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: null,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: true,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: null,
+};
+
+describe("CapSettingsSheet", () => {
+ it("renders native settings rows for Cap actions", async () => {
+ const renderer = await renderComponent(
+ ,
+ );
+
+ expect(getTextNodes(renderer.toJSON())).toEqual(
+ expect.arrayContaining([
+ "Settings",
+ "Launch review",
+ "Title",
+ "View analytics",
+ "Public link",
+ "Password",
+ "Protected",
+ "Copy link",
+ "Share",
+ "Save video",
+ "Delete Cap",
+ ]),
+ );
+ expect(
+ renderer.root.findAll((node) => getNodeType(node) === "GlassSurface"),
+ ).toHaveLength(4);
+ expect(
+ renderer.root.find((node) => getNodeType(node) === "Modal").props
+ .allowSwipeDismissal,
+ ).toBe(true);
+ const closeButton = renderer.root.findByProps({
+ accessibilityLabel: "Close Cap settings",
+ });
+ expect(closeButton.props.accessibilityHint).toBe("Dismisses Cap settings");
+ expect(closeButton.props.hitSlop).toBe(8);
+ expect(
+ renderer.root.findByProps({ accessibilityHint: "Renames this Cap" }),
+ ).toBeTruthy();
+ expect(
+ renderer.root.findByProps({
+ accessibilityHint: "Copies this Cap link",
+ }),
+ ).toBeTruthy();
+ expect(
+ renderer.root.findByProps({
+ accessibilityHint: "Opens the native share sheet",
+ }),
+ ).toBeTruthy();
+ expect(
+ renderer.root.findByProps({ accessibilityHint: "Deletes this Cap" }),
+ ).toBeTruthy();
+ });
+
+ it("updates public link with the native switch", async () => {
+ const onVisibilityChange = vi.fn();
+ const renderer = await renderComponent(
+ ,
+ );
+
+ const switchNode = renderer.root.findByType(Switch);
+ expect(switchNode.props).toMatchObject({
+ accessibilityLabel: "Public link",
+ accessibilityHint: "Toggles public link sharing",
+ accessibilityRole: "switch",
+ accessibilityState: {
+ checked: true,
+ disabled: false,
+ },
+ ios_backgroundColor: "#e0e0e0",
+ trackColor: {
+ false: "#e0e0e0",
+ true: "#8ec8f6",
+ },
+ });
+
+ switchNode.props.onValueChange(false);
+
+ expect(onVisibilityChange).toHaveBeenCalledWith(cap, false);
+ });
+
+ it("marks disabled save actions as unavailable in the native settings sheet", async () => {
+ const onSaveVideo = vi.fn();
+ const renderer = await renderComponent(
+ ,
+ );
+
+ const saveRow = renderer.root
+ .findAllByProps({ accessibilityRole: "button" })
+ .find((node) => getInstanceText(node).includes("Save video"));
+ if (!saveRow) throw new Error("Save video row was not rendered");
+
+ expect(saveRow.props.accessibilityState).toEqual({ disabled: true });
+ expect(saveRow.props.disabled).toBe(true);
+ expect(saveRow.props.accessibilityHint).toBe("Save is in progress");
+ expect(saveRow.props.accessibilityValue).toEqual({
+ text: "Saving video for Launch review",
+ });
+ expect(getInstanceText(saveRow)).not.toContain("Saving...");
+ expect(resolveStyle(saveRow.props.style)).toMatchObject({
+ backgroundColor: "#f9f9f9",
+ });
+ });
+
+ it("marks disabled sharing updates as in progress", async () => {
+ const onVisibilityChange = vi.fn();
+ const renderer = await renderComponent(
+ ,
+ );
+
+ const publicLinkRow = renderer.root
+ .findAllByProps({ accessibilityLabel: "Public link" })
+ .find((node) => getInstanceText(node).includes("Public link"));
+ if (!publicLinkRow) throw new Error("Public link row was not rendered");
+ const switchNode = renderer.root.findByType(Switch);
+ expect(publicLinkRow.props.accessibilityValue).toEqual({
+ text: "Updating sharing for Launch review",
+ });
+ expect(getInstanceText(publicLinkRow)).not.toContain("Updating...");
+ expect(switchNode.props.accessibilityState).toEqual({
+ checked: true,
+ disabled: true,
+ });
+ expect(switchNode.props.accessibilityHint).toBe(
+ "Sharing update is in progress",
+ );
+ expect(switchNode.props.disabled).toBe(true);
+ });
+
+ it("opens analytics from the native settings sheet", async () => {
+ const onViewAnalytics = vi.fn();
+ const renderer = await renderComponent(
+ ,
+ );
+
+ const analyticsRow = renderer.root
+ .findAllByProps({ accessibilityRole: "button" })
+ .find((node) => getInstanceText(node).includes("View analytics"));
+ if (!analyticsRow) throw new Error("Analytics row was not rendered");
+ expect(analyticsRow.props.accessibilityHint).toBe(
+ "Opens analytics in a browser sheet",
+ );
+
+ await act(async () => {
+ analyticsRow.props.onPress();
+ });
+
+ expect(onViewAnalytics).toHaveBeenCalledWith(cap);
+ });
+});
diff --git a/apps/mobile/src/caps/CapSettingsSheet.tsx b/apps/mobile/src/caps/CapSettingsSheet.tsx
new file mode 100644
index 00000000000..ceb1ab03bac
--- /dev/null
+++ b/apps/mobile/src/caps/CapSettingsSheet.tsx
@@ -0,0 +1,471 @@
+import { type SFSymbol, SymbolView } from "expo-symbols";
+import type { ReactNode } from "react";
+import {
+ Modal,
+ Pressable,
+ ScrollView,
+ StyleSheet,
+ Switch,
+ Text,
+ View,
+} from "react-native";
+import type { MobileCapSummary } from "@/api/mobile";
+import { GlassSurface } from "@/components/GlassSurface";
+import { colors, fonts, radius, squircle } from "@/theme";
+
+type CapSettingsSheetProps = {
+ cap: MobileCapSummary | null;
+ visible: boolean;
+ onClose: () => void;
+ onCopyLink: (cap: MobileCapSummary) => void;
+ onShareLink: (cap: MobileCapSummary) => void;
+ onRename: (cap: MobileCapSummary) => void;
+ onPassword: (cap: MobileCapSummary) => void;
+ onViewAnalytics?: (cap: MobileCapSummary) => void;
+ onVisibilityChange: (cap: MobileCapSummary, isPublic: boolean) => void;
+ onSaveVideo: (cap: MobileCapSummary) => void;
+ onDelete: (cap: MobileCapSummary) => void;
+ visibilityDisabled?: boolean;
+ visibilityDisabledHint?: string;
+ visibilityDisabledValue?: string;
+ visibilityDisabledAccessibilityValue?: string;
+ saveDisabled?: boolean;
+ saveDisabledHint?: string;
+ saveDisabledValue?: string;
+ saveDisabledAccessibilityValue?: string;
+};
+
+type SettingsRowProps = {
+ label: string;
+ value?: string;
+ accessibilityValueText?: string;
+ symbol: SFSymbol;
+ accessibilityHint?: string;
+ danger?: boolean;
+ disabled?: boolean;
+ onPress?: () => void;
+ children?: ReactNode;
+};
+
+function SettingsRow({
+ label,
+ value,
+ accessibilityValueText,
+ symbol,
+ accessibilityHint,
+ danger = false,
+ disabled = false,
+ onPress,
+ children,
+}: SettingsRowProps) {
+ const accessibilityValue = accessibilityValueText
+ ? { text: accessibilityValueText }
+ : value
+ ? { text: value }
+ : undefined;
+ const isAction = Boolean(onPress) || disabled;
+ const content = (
+ <>
+
+
+
+
+ {label}
+
+ {value ? (
+
+ {value}
+
+ ) : null}
+ {children}
+ {isAction ? (
+
+ ) : null}
+ >
+ );
+
+ if (!isAction) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return (
+ [
+ styles.row,
+ pressed && !disabled ? styles.rowPressed : null,
+ disabled ? styles.rowDisabled : null,
+ ]}
+ >
+ {content}
+
+ );
+}
+
+function SettingsSection({
+ children,
+ title,
+}: {
+ children: ReactNode;
+ title: string;
+}) {
+ return (
+
+ {title}
+
+ {children}
+
+
+ );
+}
+
+export function CapSettingsSheet({
+ cap,
+ visible,
+ onClose,
+ onCopyLink,
+ onShareLink,
+ onRename,
+ onPassword,
+ onViewAnalytics,
+ onVisibilityChange,
+ onSaveVideo,
+ onDelete,
+ visibilityDisabled = false,
+ visibilityDisabledHint,
+ visibilityDisabledValue,
+ visibilityDisabledAccessibilityValue,
+ saveDisabled = false,
+ saveDisabledHint,
+ saveDisabledValue,
+ saveDisabledAccessibilityValue,
+}: CapSettingsSheetProps) {
+ if (!cap) return null;
+
+ return (
+
+
+
+
+ Settings
+
+ {cap.title}
+
+
+ {cap.shareUrl}
+
+
+ [
+ styles.closeButton,
+ pressed ? styles.closeButtonPressed : null,
+ ]}
+ >
+
+
+
+
+
+ onRename(cap)}
+ symbol="textformat"
+ value={cap.title}
+ />
+ {onViewAnalytics ? (
+ <>
+
+ onViewAnalytics(cap)}
+ symbol="chart.bar"
+ />
+ >
+ ) : null}
+
+
+
+
+ onVisibilityChange(cap, value)}
+ trackColor={{ false: colors.gray5, true: colors.blue7 }}
+ thumbColor={colors.white}
+ value={cap.public}
+ />
+
+
+ onPassword(cap)}
+ symbol={cap.protected ? "lock.fill" : "lock.open"}
+ value={cap.protected ? "Protected" : "Off"}
+ />
+
+
+
+ onCopyLink(cap)}
+ symbol="doc.on.doc"
+ />
+
+ onShareLink(cap)}
+ symbol="square.and.arrow.up"
+ />
+
+ onSaveVideo(cap)}
+ symbol="square.and.arrow.down"
+ accessibilityValueText={saveDisabledAccessibilityValue}
+ value={saveDisabled ? saveDisabledValue : undefined}
+ />
+
+
+
+ onDelete(cap)}
+ symbol="trash"
+ />
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ sheet: {
+ flex: 1,
+ backgroundColor: colors.appBackground,
+ },
+ sheetContent: {
+ paddingHorizontal: 20,
+ paddingTop: 20,
+ paddingBottom: 28,
+ },
+ header: {
+ flexDirection: "row",
+ alignItems: "flex-start",
+ gap: 16,
+ paddingTop: 8,
+ paddingBottom: 18,
+ },
+ headerCopy: {
+ flex: 1,
+ minWidth: 0,
+ },
+ eyebrow: {
+ fontFamily: fonts.medium,
+ fontSize: 13,
+ lineHeight: 18,
+ color: colors.gray10,
+ marginBottom: 4,
+ },
+ title: {
+ fontFamily: fonts.medium,
+ fontSize: 24,
+ lineHeight: 30,
+ color: colors.gray12,
+ },
+ shareUrl: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray10,
+ marginTop: 4,
+ },
+ closeButton: {
+ width: 34,
+ height: 34,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.gray3,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray5,
+ ...squircle,
+ },
+ closeButtonPressed: {
+ backgroundColor: colors.gray5,
+ },
+ section: {
+ gap: 8,
+ marginBottom: 18,
+ },
+ sectionTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 13,
+ lineHeight: 18,
+ color: colors.gray10,
+ paddingHorizontal: 4,
+ },
+ group: {
+ overflow: "hidden",
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ ...squircle,
+ },
+ groupFallback: {
+ backgroundColor: colors.gray1,
+ },
+ row: {
+ minHeight: 54,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 12,
+ paddingHorizontal: 14,
+ paddingVertical: 10,
+ },
+ rowPressed: {
+ backgroundColor: colors.gray2,
+ },
+ rowDisabled: {
+ backgroundColor: colors.gray2,
+ },
+ rowIcon: {
+ width: 28,
+ height: 28,
+ borderRadius: radius.sm,
+ backgroundColor: colors.gray3,
+ alignItems: "center",
+ justifyContent: "center",
+ ...squircle,
+ },
+ dangerIcon: {
+ backgroundColor: colors.red3,
+ },
+ rowIconDisabled: {
+ backgroundColor: colors.gray3,
+ },
+ rowLabel: {
+ flex: 1,
+ fontFamily: fonts.regular,
+ fontSize: 16,
+ lineHeight: 22,
+ color: colors.gray12,
+ },
+ rowValue: {
+ maxWidth: "42%",
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray10,
+ },
+ rowLabelDisabled: {
+ color: colors.gray9,
+ },
+ rowValueDisabled: {
+ color: colors.gray9,
+ },
+ dangerText: {
+ color: colors.red11,
+ },
+ separator: {
+ height: StyleSheet.hairlineWidth,
+ backgroundColor: colors.gray3,
+ marginLeft: 54,
+ },
+});
diff --git a/apps/mobile/src/caps/passwordActions.test.ts b/apps/mobile/src/caps/passwordActions.test.ts
new file mode 100644
index 00000000000..95d0511aaeb
--- /dev/null
+++ b/apps/mobile/src/caps/passwordActions.test.ts
@@ -0,0 +1,116 @@
+import { Video } from "@cap/web-domain";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import type { MobileApiClient, MobileCapSummary } from "@/api/mobile";
+import { showCapPasswordActions } from "./passwordActions";
+
+const reactNativeMock = vi.hoisted(() => ({
+ ActionSheetIOS: {
+ showActionSheetWithOptions: vi.fn(),
+ },
+ Alert: {
+ alert: vi.fn(),
+ prompt: vi.fn(),
+ },
+ Platform: {
+ OS: "ios",
+ },
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ },
+}));
+
+vi.mock("react-native", () => reactNativeMock);
+
+const cap: MobileCapSummary = {
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: null,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: false,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: null,
+};
+
+describe("showCapPasswordActions", () => {
+ beforeEach(() => {
+ reactNativeMock.ActionSheetIOS.showActionSheetWithOptions.mockClear();
+ reactNativeMock.Alert.alert.mockClear();
+ reactNativeMock.Alert.prompt.mockClear();
+ });
+
+ it("uses a native secure prompt to add a Cap password", async () => {
+ const updated = { ...cap, protected: true };
+ const updateCapPassword = vi.fn(async () => updated);
+ const onUpdated = vi.fn();
+
+ showCapPasswordActions({
+ cap,
+ client: { updateCapPassword } as unknown as MobileApiClient,
+ onUpdated,
+ });
+
+ expect(reactNativeMock.Alert.prompt).toHaveBeenCalledWith(
+ "Add password",
+ "Set a password for this Cap link.",
+ expect.any(Array),
+ "secure-text",
+ );
+
+ const buttons = reactNativeMock.Alert.prompt.mock.calls[0]?.[2];
+ const saveButton = Array.isArray(buttons) ? buttons[1] : undefined;
+ saveButton?.onPress?.(" secret ");
+ await Promise.resolve();
+ await Promise.resolve();
+
+ expect(updateCapPassword).toHaveBeenCalledWith("video_123", {
+ password: "secret",
+ });
+ expect(onUpdated).toHaveBeenCalledWith(updated);
+ });
+
+ it("uses a native action sheet to remove an existing password", async () => {
+ const protectedCap = { ...cap, protected: true };
+ const updated = { ...protectedCap, protected: false };
+ const updateCapPassword = vi.fn(async () => updated);
+ const onUpdated = vi.fn();
+
+ showCapPasswordActions({
+ cap: protectedCap,
+ client: { updateCapPassword } as unknown as MobileApiClient,
+ onUpdated,
+ });
+
+ expect(
+ reactNativeMock.ActionSheetIOS.showActionSheetWithOptions,
+ ).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cancelButtonIndex: 2,
+ destructiveButtonIndex: 1,
+ options: ["Change password", "Remove password", "Cancel"],
+ title: "Password protected",
+ userInterfaceStyle: "light",
+ }),
+ expect.any(Function),
+ );
+
+ const callback =
+ reactNativeMock.ActionSheetIOS.showActionSheetWithOptions.mock
+ .calls[0]?.[1];
+ callback?.(1);
+ await Promise.resolve();
+ await Promise.resolve();
+
+ expect(updateCapPassword).toHaveBeenCalledWith("video_123", {
+ password: null,
+ });
+ expect(onUpdated).toHaveBeenCalledWith(updated);
+ });
+});
diff --git a/apps/mobile/src/caps/passwordActions.ts b/apps/mobile/src/caps/passwordActions.ts
new file mode 100644
index 00000000000..62eb4e0f811
--- /dev/null
+++ b/apps/mobile/src/caps/passwordActions.ts
@@ -0,0 +1,97 @@
+import {
+ ActionSheetIOS,
+ Alert,
+ type AlertButton,
+ Platform,
+} from "react-native";
+import type { MobileApiClient, MobileCapSummary } from "@/api/mobile";
+import { colors } from "@/theme";
+
+type CapPasswordActionsInput = {
+ cap: MobileCapSummary;
+ client: MobileApiClient;
+ onUpdated: (cap: MobileCapSummary) => void | Promise;
+};
+
+const getPasswordErrorMessage = (error: unknown) =>
+ error instanceof Error ? error.message : "Unable to update this password.";
+
+const savePassword = async ({
+ cap,
+ client,
+ onUpdated,
+ password,
+}: CapPasswordActionsInput & { password: string | null }) => {
+ try {
+ const updated = await client.updateCapPassword(cap.id, { password });
+ await onUpdated(updated);
+ } catch (error) {
+ Alert.alert("Password update failed", getPasswordErrorMessage(error));
+ }
+};
+
+const promptForPassword = (input: CapPasswordActionsInput) => {
+ const title = input.cap.protected ? "Change password" : "Add password";
+
+ if (Platform.OS !== "ios") {
+ Alert.alert("Password", "Password editing is available on iOS.");
+ return;
+ }
+
+ Alert.prompt(
+ title,
+ "Set a password for this Cap link.",
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Save",
+ onPress: (value?: string) => {
+ const password = value?.trim() ?? "";
+ if (!password) {
+ Alert.alert("Password required", "Enter a password for this Cap.");
+ return;
+ }
+ void savePassword({ ...input, password });
+ },
+ },
+ ],
+ "secure-text",
+ );
+};
+
+export const showCapPasswordActions = (input: CapPasswordActionsInput) => {
+ if (!input.cap.protected) {
+ promptForPassword(input);
+ return;
+ }
+
+ if (Platform.OS === "ios") {
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: 2,
+ destructiveButtonIndex: 1,
+ options: ["Change password", "Remove password", "Cancel"],
+ title: "Password protected",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ if (index === 0) promptForPassword(input);
+ if (index === 1) void savePassword({ ...input, password: null });
+ },
+ );
+ return;
+ }
+
+ const buttons: AlertButton[] = [
+ {
+ text: "Remove password",
+ style: "destructive",
+ onPress: () => {
+ void savePassword({ ...input, password: null });
+ },
+ },
+ { text: "Cancel", style: "cancel" },
+ ];
+ Alert.alert("Password protected", undefined, buttons);
+};
diff --git a/apps/mobile/src/caps/saveCapVideo.ts b/apps/mobile/src/caps/saveCapVideo.ts
new file mode 100644
index 00000000000..dc52b4a4248
--- /dev/null
+++ b/apps/mobile/src/caps/saveCapVideo.ts
@@ -0,0 +1,30 @@
+import * as FileSystem from "expo-file-system/legacy";
+import * as MediaLibrary from "expo-media-library";
+import type { MobileApiClient } from "@/api/mobile";
+
+export class PhotosPermissionDeniedError extends Error {
+ constructor() {
+ super("Photos access needed");
+ this.name = "PhotosPermissionDeniedError";
+ }
+}
+
+const safeFileName = (fileName: string) =>
+ fileName.replace(/[^\w.\- ]+/g, "").trim() || "Cap.mp4";
+
+export const saveCapVideoToPhotos = async (
+ client: MobileApiClient,
+ capId: string,
+) => {
+ const permission = await MediaLibrary.requestPermissionsAsync();
+ if (!permission.granted) throw new PhotosPermissionDeniedError();
+
+ const download = await client.getDownload(capId);
+ const target = `${FileSystem.documentDirectory}${safeFileName(
+ download.fileName,
+ )}`;
+ const result = await FileSystem.downloadAsync(download.url, target);
+ await MediaLibrary.saveToLibraryAsync(result.uri);
+
+ return download.fileName;
+};
diff --git a/apps/mobile/src/caps/titleActions.test.ts b/apps/mobile/src/caps/titleActions.test.ts
new file mode 100644
index 00000000000..0d9622a5a36
--- /dev/null
+++ b/apps/mobile/src/caps/titleActions.test.ts
@@ -0,0 +1,93 @@
+import { Video } from "@cap/web-domain";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import type { MobileApiClient, MobileCapSummary } from "@/api/mobile";
+import { showCapTitleActions } from "./titleActions";
+
+const reactNativeMock = vi.hoisted(() => ({
+ Alert: {
+ alert: vi.fn(),
+ prompt: vi.fn(),
+ },
+ Platform: {
+ OS: "ios",
+ },
+}));
+
+vi.mock("react-native", () => reactNativeMock);
+
+const cap: MobileCapSummary = {
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: null,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: false,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: null,
+};
+
+describe("showCapTitleActions", () => {
+ beforeEach(() => {
+ reactNativeMock.Alert.alert.mockClear();
+ reactNativeMock.Alert.prompt.mockClear();
+ reactNativeMock.Platform.OS = "ios";
+ });
+
+ it("uses a native prompt to rename a Cap", async () => {
+ const updated = { ...cap, title: "Roadmap review" };
+ const updateCapTitle = vi.fn(async () => updated);
+ const onUpdated = vi.fn();
+
+ showCapTitleActions({
+ cap,
+ client: { updateCapTitle } as unknown as MobileApiClient,
+ onUpdated,
+ });
+
+ expect(reactNativeMock.Alert.prompt).toHaveBeenCalledWith(
+ "Rename Cap",
+ undefined,
+ expect.any(Array),
+ "plain-text",
+ "Launch review",
+ );
+
+ const buttons = reactNativeMock.Alert.prompt.mock.calls[0]?.[2];
+ const saveButton = Array.isArray(buttons) ? buttons[1] : undefined;
+ saveButton?.onPress?.(" Roadmap review ");
+ await Promise.resolve();
+ await Promise.resolve();
+
+ expect(updateCapTitle).toHaveBeenCalledWith("video_123", {
+ title: "Roadmap review",
+ });
+ expect(onUpdated).toHaveBeenCalledWith(updated);
+ });
+
+ it("rejects blank Cap titles before calling the API", () => {
+ const updateCapTitle = vi.fn();
+
+ showCapTitleActions({
+ cap,
+ client: { updateCapTitle } as unknown as MobileApiClient,
+ onUpdated: vi.fn(),
+ });
+
+ const buttons = reactNativeMock.Alert.prompt.mock.calls[0]?.[2];
+ const saveButton = Array.isArray(buttons) ? buttons[1] : undefined;
+ saveButton?.onPress?.(" ");
+
+ expect(updateCapTitle).not.toHaveBeenCalled();
+ expect(reactNativeMock.Alert.alert).toHaveBeenCalledWith(
+ "Title required",
+ "Enter a title for this Cap.",
+ );
+ });
+});
diff --git a/apps/mobile/src/caps/titleActions.ts b/apps/mobile/src/caps/titleActions.ts
new file mode 100644
index 00000000000..245243a836c
--- /dev/null
+++ b/apps/mobile/src/caps/titleActions.ts
@@ -0,0 +1,53 @@
+import { Alert, Platform } from "react-native";
+import type { MobileApiClient, MobileCapSummary } from "@/api/mobile";
+
+type CapTitleActionsInput = {
+ cap: MobileCapSummary;
+ client: MobileApiClient;
+ onUpdated: (cap: MobileCapSummary) => void | Promise;
+};
+
+const getTitleErrorMessage = (error: unknown) =>
+ error instanceof Error ? error.message : "Unable to rename this Cap.";
+
+const saveTitle = async ({
+ cap,
+ client,
+ onUpdated,
+ title,
+}: CapTitleActionsInput & { title: string }) => {
+ try {
+ const updated = await client.updateCapTitle(cap.id, { title });
+ await onUpdated(updated);
+ } catch (error) {
+ Alert.alert("Rename failed", getTitleErrorMessage(error));
+ }
+};
+
+export const showCapTitleActions = (input: CapTitleActionsInput) => {
+ if (Platform.OS !== "ios") {
+ Alert.alert("Rename Cap", "Title editing is available on iOS.");
+ return;
+ }
+
+ Alert.prompt(
+ "Rename Cap",
+ undefined,
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Save",
+ onPress: (value?: string) => {
+ const title = value?.trim() ?? "";
+ if (!title) {
+ Alert.alert("Title required", "Enter a title for this Cap.");
+ return;
+ }
+ void saveTitle({ ...input, title });
+ },
+ },
+ ],
+ "plain-text",
+ input.cap.title,
+ );
+};
diff --git a/apps/mobile/src/components/ActionButton.test.tsx b/apps/mobile/src/components/ActionButton.test.tsx
new file mode 100644
index 00000000000..c735962be44
--- /dev/null
+++ b/apps/mobile/src/components/ActionButton.test.tsx
@@ -0,0 +1,147 @@
+import type { ReactElement, ReactNode } from "react";
+import TestRenderer, { act, type ReactTestRenderer } from "react-test-renderer";
+import { describe, expect, it, vi } from "vitest";
+import { ActionButton } from "./ActionButton";
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+const renderComponent = async (
+ node: ReactElement,
+): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const resolveStyle = (style: unknown): Record => {
+ const resolved =
+ typeof style === "function" ? style({ pressed: false }) : style;
+ const styles = Array.isArray(resolved) ? resolved : [resolved];
+ return Object.assign({}, ...styles.filter(Boolean));
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ ActivityIndicator: createHost("ActivityIndicator"),
+ Pressable: createHost("Pressable"),
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ hairlineWidth: 1,
+ },
+ Text: createHost("Text"),
+ View: createHost("View"),
+ };
+});
+
+vi.mock("expo-symbols", async () => {
+ const React = await import("react");
+ return {
+ SymbolView: (props: Record) =>
+ React.createElement("SymbolView", props),
+ };
+});
+
+describe("ActionButton", () => {
+ it("matches the Cap web dark button surface and clips the inset highlight", async () => {
+ const renderer = await renderComponent(
+ ,
+ );
+ const button = renderer.root.findByProps({
+ accessibilityLabel: "Upload",
+ });
+
+ expect(button.props.android_ripple).toEqual({
+ color: "rgba(18, 22, 31, 0.05)",
+ });
+ expect(button.props.hitSlop).toEqual({
+ bottom: 4,
+ left: 4,
+ right: 4,
+ top: 4,
+ });
+ expect(button.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: false,
+ });
+ expect(button.props.accessibilityHint).toBe("Opens upload options");
+ expect(resolveStyle(button.props.style)).toMatchObject({
+ backgroundColor: "#202020",
+ borderColor: "#202020",
+ borderRadius: 999,
+ height: 44,
+ overflow: "hidden",
+ });
+ });
+
+ it("uses the Cap web gray button token pair", async () => {
+ const renderer = await renderComponent(
+ ,
+ );
+ const button = renderer.root.findByProps({
+ accessibilityLabel: "Photos",
+ });
+
+ expect(resolveStyle(button.props.style)).toMatchObject({
+ backgroundColor: "#e0e0e0",
+ borderColor: "#bbbbbb",
+ });
+ });
+
+ it("allows a specific native label while keeping short visible text", async () => {
+ const renderer = await renderComponent(
+ ,
+ );
+ const button = renderer.root.findByProps({
+ accessibilityLabel: "Retry upload failed-upload.mp4",
+ });
+
+ expect(button.findByProps({ children: "Retry" }).props.children).toBe(
+ "Retry",
+ );
+ expect(button.props.accessibilityValue).toEqual({
+ text: "Upload failed",
+ });
+ });
+
+ it("exposes native disabled and busy state while loading", async () => {
+ const renderer = await renderComponent(
+ ,
+ );
+ const button = renderer.root.findByProps({
+ accessibilityLabel: "Upload",
+ });
+
+ expect(button.props.disabled).toBe(true);
+ expect(button.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ });
+});
diff --git a/apps/mobile/src/components/ActionButton.tsx b/apps/mobile/src/components/ActionButton.tsx
new file mode 100644
index 00000000000..d12855f239b
--- /dev/null
+++ b/apps/mobile/src/components/ActionButton.tsx
@@ -0,0 +1,320 @@
+import { type SFSymbol, SymbolView } from "expo-symbols";
+import type { ReactNode } from "react";
+import {
+ type AccessibilityValue,
+ ActivityIndicator,
+ type GestureResponderEvent,
+ Pressable,
+ StyleSheet,
+ Text,
+ View,
+ type ViewStyle,
+} from "react-native";
+import { colors, fonts, radius, squircle } from "@/theme";
+
+type ActionButtonProps = {
+ label: string;
+ onPress: (event?: GestureResponderEvent) => void;
+ accessibilityLabel?: string;
+ accessibilityHint?: string;
+ accessibilityValue?: AccessibilityValue;
+ symbol?: SFSymbol;
+ leading?: ReactNode;
+ variant?:
+ | "primary"
+ | "blue"
+ | "secondary"
+ | "gray"
+ | "dark"
+ | "danger"
+ | "ghost";
+ size?: "sm" | "md" | "lg";
+ disabled?: boolean;
+ loading?: boolean;
+ style?: ViewStyle;
+ children?: ReactNode;
+};
+
+const labelBySize = {
+ sm: "labelSm",
+ md: "labelMd",
+ lg: "labelLg",
+} as const;
+
+const iconColor = (
+ variant: NonNullable,
+ isDisabled: boolean,
+) => {
+ if (isDisabled) {
+ if (variant === "primary") return colors.gray9;
+ if (variant === "blue" || variant === "dark" || variant === "danger") {
+ return colors.gray10;
+ }
+ if (variant === "gray") return colors.gray11;
+ }
+
+ return variant === "primary" ||
+ variant === "blue" ||
+ variant === "dark" ||
+ variant === "danger"
+ ? colors.white
+ : colors.gray12;
+};
+
+const usesInsetHighlight = (
+ variant: NonNullable,
+) =>
+ variant === "primary" ||
+ variant === "blue" ||
+ variant === "gray" ||
+ variant === "dark";
+
+const buttonHitSlop = { bottom: 4, left: 4, right: 4, top: 4 };
+const androidRipple = { color: colors.blackAlpha5 };
+
+export function ActionButton({
+ label,
+ onPress,
+ accessibilityLabel,
+ accessibilityHint,
+ accessibilityValue,
+ symbol,
+ leading,
+ variant = "primary",
+ size = "md",
+ disabled = false,
+ loading = false,
+ style,
+ children,
+}: ActionButtonProps) {
+ const isDisabled = disabled || loading;
+ const showInsetHighlight = usesInsetHighlight(variant);
+
+ return (
+ [
+ styles.base,
+ styles[size],
+ styles[variant],
+ isDisabled && styles[`${variant}Disabled`],
+ pressed && !isDisabled && pressedStyles[variant],
+ style,
+ ]}
+ >
+ {showInsetHighlight ? (
+
+ ) : null}
+ {loading ? (
+
+ ) : leading ? (
+ leading
+ ) : symbol ? (
+
+ ) : null}
+
+ {children ?? label}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ base: {
+ alignItems: "center",
+ justifyContent: "center",
+ flexDirection: "row",
+ gap: 4,
+ position: "relative",
+ borderWidth: StyleSheet.hairlineWidth,
+ overflow: "hidden",
+ ...squircle,
+ },
+ sm: {
+ height: 40,
+ borderRadius: radius.full,
+ paddingHorizontal: 20,
+ },
+ md: {
+ height: 44,
+ borderRadius: radius.full,
+ paddingHorizontal: 20,
+ },
+ lg: {
+ height: 48,
+ borderRadius: radius.full,
+ paddingHorizontal: 20,
+ },
+ primary: {
+ backgroundColor: colors.gray12,
+ borderColor: colors.gray12,
+ },
+ primaryPressed: {
+ backgroundColor: colors.gray11,
+ borderColor: colors.gray11,
+ },
+ blue: {
+ backgroundColor: colors.buttonBlue,
+ borderColor: colors.buttonBlueBorder,
+ },
+ bluePressed: {
+ backgroundColor: colors.buttonBlueHover,
+ borderColor: colors.buttonBlueBorder,
+ },
+ dark: {
+ backgroundColor: colors.gray12,
+ borderColor: colors.gray12,
+ },
+ darkPressed: {
+ backgroundColor: colors.gray11,
+ borderColor: colors.gray11,
+ },
+ secondary: {
+ backgroundColor: colors.gray3,
+ borderColor: colors.gray5,
+ },
+ secondaryPressed: {
+ backgroundColor: colors.gray5,
+ borderColor: colors.gray6,
+ },
+ gray: {
+ backgroundColor: colors.gray5,
+ borderColor: colors.gray8,
+ },
+ grayPressed: {
+ backgroundColor: colors.gray7,
+ borderColor: colors.gray8,
+ },
+ danger: {
+ backgroundColor: colors.red9,
+ borderColor: colors.red9,
+ },
+ dangerPressed: {
+ backgroundColor: colors.red10,
+ borderColor: colors.red10,
+ },
+ ghost: {
+ backgroundColor: "transparent",
+ borderColor: "transparent",
+ },
+ ghostPressed: {
+ backgroundColor: colors.blackAlpha5,
+ borderColor: "transparent",
+ },
+ primaryDisabled: {
+ backgroundColor: colors.gray6,
+ borderColor: colors.gray6,
+ },
+ blueDisabled: {
+ backgroundColor: colors.gray7,
+ borderColor: colors.gray8,
+ },
+ darkDisabled: {
+ backgroundColor: colors.gray7,
+ borderColor: colors.gray8,
+ },
+ secondaryDisabled: {
+ backgroundColor: colors.gray8,
+ borderColor: colors.gray8,
+ },
+ grayDisabled: {
+ backgroundColor: colors.gray8,
+ borderColor: colors.gray7,
+ },
+ dangerDisabled: {
+ backgroundColor: colors.gray7,
+ borderColor: colors.gray8,
+ },
+ ghostDisabled: {
+ backgroundColor: "transparent",
+ borderColor: "transparent",
+ },
+ label: {
+ fontFamily: fonts.medium,
+ },
+ labelSm: {
+ fontSize: 14,
+ lineHeight: 18,
+ },
+ labelMd: {
+ fontSize: 14,
+ lineHeight: 20,
+ },
+ labelLg: {
+ fontSize: 16,
+ lineHeight: 22,
+ },
+ primaryLabel: {
+ color: colors.white,
+ },
+ defaultLabel: {
+ color: colors.gray12,
+ },
+ primaryDisabledLabel: {
+ color: colors.gray9,
+ },
+ blueDisabledLabel: {
+ color: colors.gray10,
+ },
+ darkDisabledLabel: {
+ color: colors.gray10,
+ },
+ secondaryDisabledLabel: {
+ color: colors.gray11,
+ },
+ grayDisabledLabel: {
+ color: colors.gray11,
+ },
+ dangerDisabledLabel: {
+ color: colors.gray10,
+ },
+ ghostDisabledLabel: {
+ color: colors.gray9,
+ },
+ insetHighlight: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ height: 1.5,
+ backgroundColor: "rgba(255, 255, 255, 0.4)",
+ },
+});
+
+const pressedStyles = {
+ primary: styles.primaryPressed,
+ blue: styles.bluePressed,
+ secondary: styles.secondaryPressed,
+ gray: styles.grayPressed,
+ dark: styles.darkPressed,
+ danger: styles.dangerPressed,
+ ghost: styles.ghostPressed,
+} as const;
diff --git a/apps/mobile/src/components/CapCard.test.ts b/apps/mobile/src/components/CapCard.test.ts
new file mode 100644
index 00000000000..fc441d203f2
--- /dev/null
+++ b/apps/mobile/src/components/CapCard.test.ts
@@ -0,0 +1,521 @@
+import { Video } from "@cap/web-domain";
+import React, { type ReactElement, type ReactNode } from "react";
+import TestRenderer, {
+ act,
+ type ReactTestRenderer,
+ type ReactTestRendererJSON,
+} from "react-test-renderer";
+import { describe, expect, it, vi } from "vitest";
+import type { MobileCapSummary } from "@/api/mobile";
+import { CapCard } from "./CapCard";
+import { getCapCardViewModel } from "./capCardViewModel";
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+type JsonNode = ReactTestRendererJSON | ReactTestRendererJSON[] | string | null;
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const renderTree = async (node: ReactElement): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return (renderer as ReactTestRenderer | null)?.toJSON() ?? null;
+};
+
+const renderComponent = async (
+ node: ReactElement,
+): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+const getTextNodes = (node: JsonNode): string[] => {
+ if (!node) return [];
+ if (typeof node === "string") return [node];
+ if (Array.isArray(node)) return node.flatMap(getTextNodes);
+ return node.children?.flatMap(getTextNodes) ?? [];
+};
+
+const hasProp = (node: JsonNode, prop: string, value: unknown): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node))
+ return node.some((item) => hasProp(item, prop, value));
+ if (node.props[prop] === value) return true;
+ return node.children?.some((child) => hasProp(child, prop, value)) ?? false;
+};
+
+const resolveStyle = (
+ style: unknown,
+ pressed = false,
+): Record => {
+ const resolved = typeof style === "function" ? style({ pressed }) : style;
+ const styles = Array.isArray(resolved) ? resolved : [resolved];
+ return Object.assign({}, ...styles.filter(Boolean));
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ ActivityIndicator: createHost("ActivityIndicator"),
+ Pressable: createHost("Pressable"),
+ StyleSheet: {
+ absoluteFillObject: {
+ bottom: 0,
+ left: 0,
+ position: "absolute",
+ right: 0,
+ top: 0,
+ },
+ create: >(styles: T) => styles,
+ hairlineWidth: 1,
+ },
+ Text: createHost("Text"),
+ View: createHost("View"),
+ };
+});
+
+vi.mock("expo-image", async () => {
+ const React = await import("react");
+ return {
+ Image: (props: Record) =>
+ React.createElement("Image", props),
+ };
+});
+
+vi.mock("expo-symbols", async () => {
+ const React = await import("react");
+ return {
+ SymbolView: (props: Record) =>
+ React.createElement("SymbolView", props),
+ };
+});
+
+vi.mock("react-native-svg", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ default: createHost("Svg"),
+ Circle: createHost("Circle"),
+ };
+});
+
+const cap: MobileCapSummary = {
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: 125,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: false,
+ viewCount: 7,
+ commentCount: 2,
+ reactionCount: 3,
+ upload: null,
+};
+
+describe("getCapCardViewModel", () => {
+ it("formats card rendering state", () => {
+ expect(
+ getCapCardViewModel(cap, new Date("2026-05-18T11:00:00.000Z")),
+ ).toMatchObject({
+ date: "an hour ago",
+ duration: "2 mins",
+ visibility: "Shared",
+ accessibilityLabel: "Launch review, an hour ago, Shared",
+ });
+ });
+
+ it("formats active upload state for the thumbnail overlay", () => {
+ expect(
+ getCapCardViewModel(
+ {
+ ...cap,
+ upload: {
+ uploaded: 25,
+ total: 100,
+ phase: "uploading",
+ processingProgress: 0,
+ processingMessage: null,
+ processingError: null,
+ },
+ },
+ new Date("2026-05-18T11:00:00.000Z"),
+ ),
+ ).toMatchObject({
+ uploadStatusText: "25% uploaded",
+ uploadProgress: 25,
+ uploadFailed: false,
+ accessibilityLabel: "Launch review, an hour ago, Shared, 25% uploaded",
+ });
+ });
+
+ it("keeps password protection separate from sharing state", () => {
+ expect(
+ getCapCardViewModel(
+ {
+ ...cap,
+ public: false,
+ protected: true,
+ },
+ new Date("2026-05-18T11:00:00.000Z"),
+ ),
+ ).toMatchObject({
+ date: "an hour ago",
+ visibility: "Not shared",
+ accessibilityLabel: "Launch review, an hour ago, Not shared",
+ });
+ });
+
+ it("uses processing progress as a percent value", () => {
+ expect(
+ getCapCardViewModel({
+ ...cap,
+ upload: {
+ uploaded: 100,
+ total: 100,
+ phase: "processing",
+ processingProgress: 42,
+ processingMessage: "Processing",
+ processingError: null,
+ },
+ }).uploadProgress,
+ ).toBe(42);
+ });
+
+ it("keeps non-finite upload progress display-safe", () => {
+ const uploading = getCapCardViewModel(
+ {
+ ...cap,
+ upload: {
+ uploaded: Number.NaN,
+ total: Number.NaN,
+ phase: "uploading",
+ processingProgress: 0,
+ processingMessage: null,
+ processingError: null,
+ },
+ },
+ new Date("2026-05-18T11:00:00.000Z"),
+ );
+ const processing = getCapCardViewModel({
+ ...cap,
+ upload: {
+ uploaded: 100,
+ total: 100,
+ phase: "processing",
+ processingProgress: Number.POSITIVE_INFINITY,
+ processingMessage: "Processing",
+ processingError: null,
+ },
+ });
+
+ expect(uploading).toMatchObject({
+ uploadStatusText: "0% uploaded",
+ uploadProgress: 0,
+ accessibilityLabel: "Launch review, an hour ago, Shared, 0% uploaded",
+ });
+ expect(processing.uploadProgress).toBe(0);
+ });
+
+ it("matches the web finishing state for completed processing records", () => {
+ expect(
+ getCapCardViewModel({
+ ...cap,
+ upload: {
+ uploaded: 100,
+ total: 100,
+ phase: "complete",
+ processingProgress: 100,
+ processingMessage: null,
+ processingError: null,
+ },
+ }).uploadStatusText,
+ ).toBe("Finishing up");
+ });
+});
+
+describe("CapCard", () => {
+ it("uses a branded thumbnail placeholder when a Cap has no thumbnail", async () => {
+ const tree = await renderTree(
+ React.createElement(CapCard, {
+ cap,
+ onPress: vi.fn(),
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+
+ expect(hasProp(tree, "fill", "#cecece")).toBe(true);
+ expect(hasProp(tree, "name", "play.fill")).toBe(false);
+ });
+
+ it("exposes active upload progress as a native progressbar", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapCard, {
+ cap: {
+ ...cap,
+ upload: {
+ uploaded: 25,
+ total: 100,
+ phase: "uploading",
+ processingProgress: 0,
+ processingMessage: null,
+ processingError: null,
+ },
+ },
+ onPress: vi.fn(),
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+ const [progress] = renderer.root.findAllByProps({
+ accessibilityLabel: "Upload progress",
+ });
+ if (!progress) throw new Error("Upload progress was not rendered");
+
+ expect(progress.props.accessibilityRole).toBe("progressbar");
+ expect(progress.props.accessibilityValue).toEqual({
+ max: 100,
+ min: 0,
+ now: 25,
+ text: "25%",
+ });
+ });
+
+ it("exposes processing upload state as an indeterminate progressbar", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapCard, {
+ cap: {
+ ...cap,
+ upload: {
+ uploaded: 100,
+ total: 100,
+ phase: "processing",
+ processingProgress: 0,
+ processingMessage: "Processing",
+ processingError: null,
+ },
+ },
+ onPress: vi.fn(),
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+ const [progress] = renderer.root.findAllByProps({
+ accessibilityLabel: "Upload progress",
+ });
+ if (!progress) throw new Error("Upload progress was not rendered");
+
+ expect(progress.props.accessibilityRole).toBe("progressbar");
+ expect(progress.props.accessibilityValue).toEqual({
+ text: "Processing",
+ });
+ });
+
+ it("shows copy, share, and more actions together", async () => {
+ const tree = await renderTree(
+ React.createElement(CapCard, {
+ cap,
+ onPress: vi.fn(),
+ onCopyPress: vi.fn(),
+ onSharePress: vi.fn(),
+ onMenuPress: vi.fn(),
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+
+ expect(
+ hasProp(tree, "accessibilityLabel", "Copy link for Launch review"),
+ ).toBe(true);
+ expect(hasProp(tree, "accessibilityHint", "Copies this Cap link")).toBe(
+ true,
+ );
+ expect(hasProp(tree, "accessibilityLabel", "Share Launch review")).toBe(
+ true,
+ );
+ expect(
+ hasProp(tree, "accessibilityHint", "Opens the native share sheet"),
+ ).toBe(true);
+ expect(
+ hasProp(tree, "accessibilityLabel", "More actions for Launch review"),
+ ).toBe(true);
+ expect(hasProp(tree, "accessibilityHint", "Opens Cap actions")).toBe(true);
+ });
+
+ it("uses the Cap web neutral button surface for card actions", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapCard, {
+ cap,
+ onPress: vi.fn(),
+ onCopyPress: vi.fn(),
+ onSharePress: vi.fn(),
+ onMenuPress: vi.fn(),
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+ const [copyButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Copy link for Launch review",
+ });
+ if (!copyButton) throw new Error("Copy action was not rendered");
+
+ expect(resolveStyle(copyButton.props.style)).toMatchObject({
+ width: 32,
+ height: 32,
+ backgroundColor: "#f0f0f0",
+ borderColor: "#e0e0e0",
+ });
+ expect(resolveStyle(copyButton.props.style, true)).toMatchObject({
+ backgroundColor: "#e0e0e0",
+ borderColor: "#cecece",
+ });
+ expect(copyButton.props.hitSlop).toEqual({
+ bottom: 6,
+ left: 6,
+ right: 6,
+ top: 6,
+ });
+ });
+
+ it("opens visibility controls from the shared status row like the web card", async () => {
+ const onVisibilityPress = vi.fn();
+ const renderer = await renderComponent(
+ React.createElement(CapCard, {
+ cap,
+ onPress: vi.fn(),
+ onVisibilityPress,
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+ const [shareState] = renderer.root.findAllByProps({
+ accessibilityLabel: "Change sharing for Launch review",
+ });
+ if (!shareState) throw new Error("Shared status action was not rendered");
+ const stopPropagation = vi.fn();
+
+ expect(shareState.props.accessibilityHint).toBe("Opens sharing settings");
+ expect(shareState.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: false,
+ });
+ expect(shareState.props.hitSlop).toEqual({
+ bottom: 6,
+ left: 6,
+ right: 6,
+ top: 6,
+ });
+
+ await act(async () => {
+ shareState.props.onPress({ stopPropagation });
+ });
+
+ expect(stopPropagation).toHaveBeenCalled();
+ expect(onVisibilityPress).toHaveBeenCalledTimes(1);
+ });
+
+ it("shows a disabled sharing state while the card visibility is updating", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapCard, {
+ cap,
+ onPress: vi.fn(),
+ onVisibilityPress: vi.fn(),
+ visibilityBusy: true,
+ visibilityDisabled: true,
+ visibilityDisabledHint: "Sharing update is in progress",
+ visibilityAccessibilityValue: "Updating sharing for Launch review",
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+ const [shareState] = renderer.root.findAllByProps({
+ accessibilityLabel: "Change sharing for Launch review",
+ });
+ if (!shareState) throw new Error("Shared status action was not rendered");
+
+ expect(getTextNodes(renderer.toJSON())).toContain("Shared");
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Updating...");
+ expect(shareState.props.disabled).toBe(true);
+ expect(shareState.props.accessibilityHint).toBe(
+ "Sharing update is in progress",
+ );
+ expect(shareState.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(shareState.props.accessibilityValue).toEqual({
+ text: "Updating sharing for Launch review",
+ });
+ expect(resolveStyle(shareState.props.style, true)).toMatchObject({
+ backgroundColor: "#f9f9f9",
+ });
+ });
+
+ it("opens analytics from the metrics row like the web card", async () => {
+ const onAnalyticsPress = vi.fn();
+ const renderer = await renderComponent(
+ React.createElement(CapCard, {
+ cap,
+ onAnalyticsPress,
+ onPress: vi.fn(),
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+ const [metricsRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "View analytics for Launch review",
+ });
+ if (!metricsRow) throw new Error("Analytics action was not rendered");
+ const stopPropagation = vi.fn();
+
+ expect(metricsRow.props.accessibilityHint).toBe(
+ "Opens analytics in a browser sheet",
+ );
+ expect(metricsRow.props.accessibilityState).toEqual({
+ disabled: false,
+ });
+
+ await act(async () => {
+ metricsRow.props.onPress({ stopPropagation });
+ });
+
+ expect(stopPropagation).toHaveBeenCalled();
+ expect(onAnalyticsPress).toHaveBeenCalledTimes(1);
+ });
+
+ it("marks metrics as disabled when analytics are informational only", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapCard, {
+ cap,
+ onPress: vi.fn(),
+ now: new Date("2026-05-18T11:00:00.000Z"),
+ }),
+ );
+ const [metricsRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "View analytics for Launch review",
+ });
+ if (!metricsRow) throw new Error("Metrics row was not rendered");
+
+ expect(metricsRow.props.disabled).toBe(true);
+ expect(metricsRow.props.accessibilityHint).toBeUndefined();
+ expect(metricsRow.props.accessibilityState).toEqual({
+ disabled: true,
+ });
+ });
+});
diff --git a/apps/mobile/src/components/CapCard.tsx b/apps/mobile/src/components/CapCard.tsx
new file mode 100644
index 00000000000..16b698c5b4f
--- /dev/null
+++ b/apps/mobile/src/components/CapCard.tsx
@@ -0,0 +1,649 @@
+import { Image } from "expo-image";
+import { SymbolView } from "expo-symbols";
+import { useEffect, useRef, useState } from "react";
+import {
+ ActivityIndicator,
+ Pressable,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+import Svg, { Circle } from "react-native-svg";
+import type { MobileCapSummary } from "@/api/mobile";
+import { colors, fonts, radius, squircle } from "@/theme";
+import { getCapCardViewModel } from "./capCardViewModel";
+
+type CapCardProps = {
+ cap: MobileCapSummary;
+ onPress: () => void;
+ onCopyPress?: () => void;
+ onSharePress?: () => void;
+ onVisibilityPress?: () => void;
+ onAnalyticsPress?: () => void;
+ onMenuPress?: () => void;
+ visibilityBusy?: boolean;
+ visibilityDisabled?: boolean;
+ visibilityDisabledHint?: string;
+ visibilityValue?: string;
+ visibilityAccessibilityValue?: string;
+ now?: Date;
+};
+
+const progressSize = 18;
+const progressStrokeWidth = 3;
+const progressRadius = (progressSize - progressStrokeWidth) / 2;
+const progressCircumference = 2 * Math.PI * progressRadius;
+const compactHitSlop = { bottom: 6, left: 6, right: 6, top: 6 };
+
+const getProgressAccessibilityValue = (
+ progress: number | null,
+ indeterminate: boolean,
+ statusText: string,
+) => {
+ if (indeterminate || progress === null) {
+ return { text: statusText };
+ }
+
+ const clampedProgress = Math.min(100, Math.max(0, progress));
+
+ return {
+ max: 100,
+ min: 0,
+ now: clampedProgress,
+ text: `${clampedProgress}%`,
+ };
+};
+
+function CapThumbnailPlaceholder() {
+ return (
+
+
+
+
+
+
+ );
+}
+
+function UploadProgressIndicator({
+ progress,
+ indeterminate,
+ statusText,
+}: {
+ progress: number | null;
+ indeterminate: boolean;
+ statusText: string;
+}) {
+ const accessibilityValue = getProgressAccessibilityValue(
+ progress,
+ indeterminate,
+ statusText,
+ );
+
+ if (indeterminate || progress === null) {
+ return (
+
+
+
+ );
+ }
+
+ const strokeDashoffset =
+ progressCircumference -
+ (Math.min(100, Math.max(0, progress)) / 100) * progressCircumference;
+
+ return (
+
+
+
+
+
+ );
+}
+
+export function CapCard({
+ cap,
+ onPress,
+ onCopyPress,
+ onSharePress,
+ onVisibilityPress,
+ onAnalyticsPress,
+ onMenuPress,
+ visibilityBusy = false,
+ visibilityDisabled = false,
+ visibilityDisabledHint,
+ visibilityValue,
+ visibilityAccessibilityValue,
+ now,
+}: CapCardProps) {
+ const viewModel = getCapCardViewModel(cap, now);
+ const [copyPressed, setCopyPressed] = useState(false);
+ const copyResetTimer = useRef | null>(null);
+ const hasCopyAction = Boolean(onCopyPress);
+ const hasShareAction = Boolean(onSharePress);
+ const hasVisibleMenuAction = Boolean(onMenuPress);
+ const hasActions = hasCopyAction || hasShareAction || hasVisibleMenuAction;
+ const visibilityActionDisabled = visibilityDisabled || visibilityBusy;
+ const visibilityHint = visibilityActionDisabled
+ ? (visibilityDisabledHint ?? "Sharing update is in progress")
+ : "Opens sharing settings";
+ const visibilityText = visibilityValue ?? viewModel.visibility;
+ const uploadIndeterminate =
+ Boolean(cap.upload) &&
+ cap.upload?.phase !== "uploading" &&
+ (viewModel.uploadProgress ?? 0) === 0;
+
+ useEffect(
+ () => () => {
+ if (copyResetTimer.current) clearTimeout(copyResetTimer.current);
+ },
+ [],
+ );
+
+ const copyLink = () => {
+ if (!onCopyPress) return;
+ onCopyPress();
+ setCopyPressed(true);
+ if (copyResetTimer.current) clearTimeout(copyResetTimer.current);
+ copyResetTimer.current = setTimeout(() => {
+ setCopyPressed(false);
+ copyResetTimer.current = null;
+ }, 1400);
+ };
+
+ return (
+ [styles.card, pressed && styles.pressed]}
+ >
+
+ {hasActions ? (
+
+ {onCopyPress ? (
+ {
+ event.stopPropagation();
+ copyLink();
+ }}
+ style={({ pressed }) => [
+ styles.actionIconButton,
+ pressed && styles.actionIconButtonPressed,
+ ]}
+ >
+
+
+ ) : null}
+ {onSharePress ? (
+ {
+ event.stopPropagation();
+ onSharePress();
+ }}
+ style={({ pressed }) => [
+ styles.actionIconButton,
+ pressed && styles.actionIconButtonPressed,
+ ]}
+ >
+
+
+ ) : null}
+ {onMenuPress ? (
+ {
+ event.stopPropagation();
+ onMenuPress();
+ }}
+ style={({ pressed }) => [
+ styles.actionIconButton,
+ pressed && styles.actionIconButtonPressed,
+ ]}
+ >
+
+
+ ) : null}
+
+ ) : null}
+ {cap.thumbnailUrl ? (
+
+ ) : (
+
+ )}
+ {viewModel.uploadStatusText ? (
+
+
+
+ {viewModel.uploadStatusText}
+
+ {viewModel.uploadFailed ? null : (
+
+ )}
+
+
+ ) : null}
+ {cap.protected ? (
+
+
+
+ ) : null}
+ {viewModel.duration ? (
+
+ {viewModel.duration}
+
+ ) : null}
+
+
+
+
+ {cap.title}
+
+ {onVisibilityPress ? (
+ {
+ event.stopPropagation();
+ onVisibilityPress();
+ }}
+ style={({ pressed }) => [
+ styles.shareStateButton,
+ pressed &&
+ !visibilityActionDisabled &&
+ styles.shareStateButtonPressed,
+ visibilityActionDisabled && styles.shareStateButtonDisabled,
+ ]}
+ >
+
+ {visibilityText}
+
+
+
+ ) : (
+
+ {viewModel.visibility}
+
+ )}
+
+ {viewModel.date}
+
+
+ {
+ event.stopPropagation();
+ onAnalyticsPress?.();
+ }}
+ style={({ pressed }) => [
+ styles.metricsRow,
+ onAnalyticsPress && styles.metricsRowAction,
+ pressed && onAnalyticsPress && styles.metricsRowPressed,
+ ]}
+ >
+
+
+ {cap.viewCount}
+
+
+
+ {cap.commentCount}
+
+
+
+ {cap.reactionCount}
+
+ {onAnalyticsPress ? (
+ View analytics
+ ) : null}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ card: {
+ backgroundColor: colors.gray1,
+ borderRadius: radius.md,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray3,
+ overflow: "hidden",
+ marginBottom: 16,
+ ...squircle,
+ },
+ pressed: {
+ backgroundColor: colors.gray2,
+ borderColor: colors.blue10,
+ },
+ thumbnailWrap: {
+ width: "100%",
+ aspectRatio: 16 / 9,
+ backgroundColor: colors.black,
+ position: "relative",
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: colors.gray3,
+ },
+ thumbnail: {
+ width: "100%",
+ height: "100%",
+ },
+ emptyThumbnail: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.gray3,
+ overflow: "hidden",
+ },
+ placeholderSheen: {
+ position: "absolute",
+ top: -36,
+ left: -28,
+ width: "78%",
+ height: "140%",
+ backgroundColor: "rgba(255, 255, 255, 0.34)",
+ transform: [{ rotate: "18deg" }],
+ },
+ placeholderMark: {
+ width: 48,
+ height: 48,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: "rgba(255, 255, 255, 0.72)",
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: "rgba(255, 255, 255, 0.95)",
+ ...squircle,
+ },
+ durationPill: {
+ position: "absolute",
+ left: 12,
+ bottom: 12,
+ minWidth: 46,
+ height: 23,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
+ paddingHorizontal: 8,
+ ...squircle,
+ },
+ durationText: {
+ fontFamily: fonts.medium,
+ fontSize: 11,
+ color: colors.white,
+ },
+ lockBadge: {
+ position: "absolute",
+ right: 10,
+ top: 10,
+ zIndex: 2,
+ width: 28,
+ height: 28,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
+ },
+ lockBadgeWithActions: {
+ right: 46,
+ },
+ actionStack: {
+ position: "absolute",
+ right: 10,
+ top: 10,
+ zIndex: 2,
+ gap: 8,
+ },
+ actionIconButton: {
+ width: 32,
+ height: 32,
+ borderRadius: radius.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.gray3,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray5,
+ shadowColor: colors.black,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 8,
+ elevation: 2,
+ ...squircle,
+ },
+ actionIconButtonPressed: {
+ backgroundColor: colors.gray5,
+ borderColor: colors.gray7,
+ },
+ uploadOverlay: {
+ ...StyleSheet.absoluteFillObject,
+ justifyContent: "flex-end",
+ backgroundColor: "rgba(0, 0, 0, 0.58)",
+ paddingHorizontal: 12,
+ paddingBottom: 12,
+ zIndex: 1,
+ },
+ uploadStatusRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ paddingRight: 96,
+ },
+ uploadStatusText: {
+ fontFamily: fonts.medium,
+ fontSize: 14,
+ lineHeight: 19,
+ color: colors.white,
+ },
+ progressIndicator: {
+ width: progressSize,
+ height: progressSize,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ progressRing: {
+ transform: [{ rotate: "-90deg" }],
+ },
+ body: {
+ paddingHorizontal: 16,
+ paddingBottom: 16,
+ gap: 12,
+ },
+ title: {
+ fontFamily: fonts.medium,
+ fontSize: 16,
+ lineHeight: 21,
+ color: colors.gray12,
+ marginTop: 13,
+ marginBottom: 4,
+ },
+ shareState: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 19,
+ color: colors.gray10,
+ },
+ shareStateButton: {
+ alignSelf: "flex-start",
+ minHeight: 22,
+ maxWidth: "100%",
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 5,
+ marginBottom: 2,
+ borderRadius: radius.xs,
+ paddingHorizontal: 3,
+ marginLeft: -3,
+ ...squircle,
+ },
+ shareStateButtonPressed: {
+ backgroundColor: colors.gray3,
+ },
+ shareStateButtonDisabled: {
+ backgroundColor: colors.gray2,
+ },
+ shareStateDisabled: {
+ color: colors.gray8,
+ },
+ meta: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray10,
+ },
+ metricsRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 16,
+ minHeight: 24,
+ },
+ metricsRowAction: {
+ width: "100%",
+ maxWidth: "100%",
+ borderRadius: radius.xs,
+ paddingHorizontal: 3,
+ marginLeft: -3,
+ ...squircle,
+ },
+ metricsRowPressed: {
+ backgroundColor: colors.gray3,
+ },
+ metric: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 7,
+ },
+ metricText: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ color: colors.gray12,
+ },
+ analyticsLink: {
+ marginLeft: "auto",
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ lineHeight: 17,
+ color: colors.blue11,
+ },
+});
diff --git a/apps/mobile/src/components/CapLogoBadge.tsx b/apps/mobile/src/components/CapLogoBadge.tsx
new file mode 100644
index 00000000000..7393a5b2e3f
--- /dev/null
+++ b/apps/mobile/src/components/CapLogoBadge.tsx
@@ -0,0 +1,32 @@
+import Svg, { Path, Rect } from "react-native-svg";
+import { colors } from "@/theme";
+
+type CapLogoBadgeProps = {
+ size?: number;
+};
+
+export function CapLogoBadge({ size = 48 }: CapLogoBadgeProps) {
+ return (
+
+ );
+}
diff --git a/apps/mobile/src/components/CapRefreshControl.test.tsx b/apps/mobile/src/components/CapRefreshControl.test.tsx
new file mode 100644
index 00000000000..372a9adf588
--- /dev/null
+++ b/apps/mobile/src/components/CapRefreshControl.test.tsx
@@ -0,0 +1,57 @@
+import type { ReactElement, ReactNode } from "react";
+import { RefreshControl } from "react-native";
+import TestRenderer, { act, type ReactTestRenderer } from "react-test-renderer";
+import { describe, expect, it, vi } from "vitest";
+import { CapRefreshControl } from "./CapRefreshControl";
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const renderComponent = async (
+ node: ReactElement,
+): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ RefreshControl: createHost("RefreshControl"),
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ },
+ };
+});
+
+describe("CapRefreshControl", () => {
+ it("uses Cap web colors for native pull-to-refresh", async () => {
+ const onRefresh = vi.fn();
+ const renderer = await renderComponent(
+ ,
+ );
+ const refreshControl = renderer.root.findByType(RefreshControl);
+
+ expect(refreshControl.props).toMatchObject({
+ colors: ["#0d74ce"],
+ onRefresh,
+ progressBackgroundColor: "#fcfcfc",
+ refreshing: true,
+ tintColor: "#0d74ce",
+ });
+ });
+});
diff --git a/apps/mobile/src/components/CapRefreshControl.tsx b/apps/mobile/src/components/CapRefreshControl.tsx
new file mode 100644
index 00000000000..6c55078bc4a
--- /dev/null
+++ b/apps/mobile/src/components/CapRefreshControl.tsx
@@ -0,0 +1,22 @@
+import { RefreshControl } from "react-native";
+import { colors } from "@/theme";
+
+type CapRefreshControlProps = {
+ refreshing: boolean;
+ onRefresh: () => void;
+};
+
+export function CapRefreshControl({
+ refreshing,
+ onRefresh,
+}: CapRefreshControlProps) {
+ return (
+
+ );
+}
diff --git a/apps/mobile/src/components/GlassSurface.tsx b/apps/mobile/src/components/GlassSurface.tsx
new file mode 100644
index 00000000000..9ee85941ace
--- /dev/null
+++ b/apps/mobile/src/components/GlassSurface.tsx
@@ -0,0 +1,72 @@
+import {
+ GlassView,
+ isGlassEffectAPIAvailable,
+ isLiquidGlassAvailable,
+} from "expo-glass-effect";
+import type { ReactNode } from "react";
+import {
+ Platform,
+ type StyleProp,
+ StyleSheet,
+ View,
+ type ViewStyle,
+} from "react-native";
+import { colors } from "@/theme";
+
+type GlassSurfaceProps = {
+ children?: ReactNode;
+ style?: StyleProp;
+ fallbackStyle?: StyleProp;
+ glassEffectStyle?: "clear" | "regular" | "none";
+ tintColor?: string;
+ isInteractive?: boolean;
+};
+
+const getGlassAvailable = () => {
+ if (Platform.OS !== "ios") return false;
+ try {
+ return isGlassEffectAPIAvailable() && isLiquidGlassAvailable();
+ } catch {
+ return false;
+ }
+};
+
+const glassAvailable = getGlassAvailable();
+
+export function GlassSurface({
+ children,
+ style,
+ fallbackStyle,
+ glassEffectStyle = "regular",
+ tintColor = colors.glass,
+ isInteractive = false,
+}: GlassSurfaceProps) {
+ if (glassAvailable) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ surface: {
+ overflow: "hidden",
+ },
+ fallback: {
+ backgroundColor: colors.glass,
+ },
+});
diff --git a/apps/mobile/src/components/OrgSwitcher.test.tsx b/apps/mobile/src/components/OrgSwitcher.test.tsx
new file mode 100644
index 00000000000..57f01ea78c0
--- /dev/null
+++ b/apps/mobile/src/components/OrgSwitcher.test.tsx
@@ -0,0 +1,188 @@
+import { Organisation, User } from "@cap/web-domain";
+import type { ReactElement, ReactNode } from "react";
+import TestRenderer, {
+ act,
+ type ReactTestRenderer,
+ type ReactTestRendererJSON,
+} from "react-test-renderer";
+import { describe, expect, it, vi } from "vitest";
+import type { MobileBootstrapResponse } from "@/api/mobile";
+import { OrgSwitcher } from "./OrgSwitcher";
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+type JsonNode = ReactTestRendererJSON | ReactTestRendererJSON[] | string | null;
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const renderTree = async (node: ReactElement): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return (renderer as ReactTestRenderer | null)?.toJSON() ?? null;
+};
+
+const renderComponent = async (
+ node: ReactElement,
+): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+const hasImageSourceUri = (node: JsonNode, uri: string): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node))
+ return node.some((item) => hasImageSourceUri(item, uri));
+ const source = node.props.source;
+ if (
+ source &&
+ typeof source === "object" &&
+ "uri" in source &&
+ source.uri === uri
+ ) {
+ return true;
+ }
+ return node.children?.some((child) => hasImageSourceUri(child, uri)) ?? false;
+};
+
+const resolveStyle = (
+ style: unknown,
+ pressed = false,
+): Record => {
+ const resolved = typeof style === "function" ? style({ pressed }) : style;
+ const styles = Array.isArray(resolved) ? resolved : [resolved];
+ return Object.assign({}, ...styles.filter(Boolean));
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ ActionSheetIOS: {
+ showActionSheetWithOptions: vi.fn(),
+ },
+ Modal: createHost("Modal"),
+ Platform: {
+ OS: "ios",
+ },
+ Pressable: createHost("Pressable"),
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ hairlineWidth: 1,
+ },
+ Text: createHost("Text"),
+ View: createHost("View"),
+ };
+});
+
+vi.mock("expo-image", async () => {
+ const React = await import("react");
+ return {
+ Image: (props: Record) =>
+ React.createElement("Image", props),
+ };
+});
+
+vi.mock("expo-symbols", async () => {
+ const React = await import("react");
+ return {
+ SymbolView: (props: Record) =>
+ React.createElement("SymbolView", props),
+ };
+});
+
+const bootstrap: MobileBootstrapResponse = {
+ user: {
+ id: User.UserId.make("user_123"),
+ name: "Richie",
+ email: "richie@cap.so",
+ imageUrl: null,
+ activeOrganizationId: Organisation.OrganisationId.make("org_123"),
+ },
+ organizations: [
+ {
+ id: Organisation.OrganisationId.make("org_123"),
+ name: "Cap",
+ iconUrl: "https://cap.so/icon.png",
+ role: "owner",
+ },
+ {
+ id: Organisation.OrganisationId.make("org_456"),
+ name: "Design",
+ iconUrl: null,
+ role: "member",
+ },
+ ],
+ activeOrganizationId: Organisation.OrganisationId.make("org_123"),
+ rootFolders: [],
+};
+
+describe("OrgSwitcher", () => {
+ it("uses the organization icon when the active org has one", async () => {
+ const tree = await renderTree(
+ ,
+ );
+
+ expect(hasImageSourceUri(tree, "https://cap.so/icon.png")).toBe(true);
+ });
+
+ it("uses a native organization action sheet with roles and disabled active org", async () => {
+ const onChange = vi.fn(() => Promise.resolve());
+ const renderer = await renderComponent(
+ ,
+ );
+ const [trigger] = renderer.root.findAllByProps({
+ accessibilityLabel: "Switch organization",
+ });
+ if (!trigger) throw new Error("Organization switcher was not rendered");
+
+ expect(resolveStyle(trigger.props.style, true)).toMatchObject({
+ backgroundColor: "#f0f0f0",
+ borderColor: "#d9d9d9",
+ });
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ trigger.props.onPress();
+ });
+
+ expect(showActionSheetWithOptions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cancelButtonIndex: 2,
+ disabledButtonIndices: [0],
+ disabledButtonTintColor: "#8d8d8d",
+ options: ["Cap (Owner)", "Design (Member)", "Cancel"],
+ title: "Organization",
+ userInterfaceStyle: "light",
+ }),
+ expect.any(Function),
+ );
+
+ const [, callback] = showActionSheetWithOptions.mock.calls[0] ?? [];
+ if (!callback) throw new Error("Organization action sheet did not open");
+
+ await act(async () => {
+ callback(1);
+ });
+
+ expect(onChange).toHaveBeenCalledWith("org_456");
+ });
+});
diff --git a/apps/mobile/src/components/OrgSwitcher.tsx b/apps/mobile/src/components/OrgSwitcher.tsx
new file mode 100644
index 00000000000..b87f5391fa8
--- /dev/null
+++ b/apps/mobile/src/components/OrgSwitcher.tsx
@@ -0,0 +1,246 @@
+import { Image } from "expo-image";
+import { SymbolView } from "expo-symbols";
+import { useState } from "react";
+import {
+ ActionSheetIOS,
+ Modal,
+ Platform,
+ Pressable,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+import type { MobileBootstrapResponse } from "@/api/mobile";
+import { colors, fonts, radius, shadows, squircle } from "@/theme";
+
+type OrgSwitcherProps = {
+ bootstrap: MobileBootstrapResponse;
+ onChange: (organizationId: string) => Promise;
+};
+
+type Organization = MobileBootstrapResponse["organizations"][number];
+
+const formatRole = (role: Organization["role"]) =>
+ role.slice(0, 1).toUpperCase() + role.slice(1);
+
+function OrgAvatar({ organization }: { organization: Organization }) {
+ return (
+
+ {organization.iconUrl ? (
+
+ ) : (
+
+ {organization.name.slice(0, 1).toUpperCase()}
+
+ )}
+
+ );
+}
+
+export function OrgSwitcher({ bootstrap, onChange }: OrgSwitcherProps) {
+ const [open, setOpen] = useState(false);
+ const activeOrganization =
+ bootstrap.organizations.find(
+ (org) => org.id === bootstrap.activeOrganizationId,
+ ) ?? bootstrap.organizations[0];
+
+ if (!activeOrganization) return null;
+
+ const openSwitcher = () => {
+ if (Platform.OS === "ios") {
+ const activeIndex = bootstrap.organizations.findIndex(
+ (organization) => organization.id === activeOrganization.id,
+ );
+ const options = [
+ ...bootstrap.organizations.map(
+ (organization) =>
+ `${organization.name} (${formatRole(organization.role)})`,
+ ),
+ "Cancel",
+ ];
+ ActionSheetIOS.showActionSheetWithOptions(
+ {
+ cancelButtonIndex: options.length - 1,
+ disabledButtonIndices: activeIndex >= 0 ? [activeIndex] : undefined,
+ disabledButtonTintColor: colors.gray9,
+ message: activeOrganization.name,
+ options,
+ title: "Organization",
+ tintColor: colors.blue11,
+ userInterfaceStyle: "light",
+ },
+ (index) => {
+ const organization = bootstrap.organizations[index];
+ if (organization && organization.id !== activeOrganization.id) {
+ void onChange(organization.id);
+ }
+ },
+ );
+ return;
+ }
+ setOpen(true);
+ };
+
+ return (
+ <>
+ [
+ styles.trigger,
+ pressed && styles.triggerPressed,
+ ]}
+ >
+
+
+ {activeOrganization.name}
+
+
+
+ setOpen(false)}
+ presentationStyle="overFullScreen"
+ transparent
+ visible={open}
+ >
+ setOpen(false)}>
+
+ Organization
+ {bootstrap.organizations.map((org) => {
+ const active = org.id === activeOrganization.id;
+ return (
+ {
+ setOpen(false);
+ if (!active) await onChange(org.id);
+ }}
+ style={styles.orgRow}
+ >
+
+
+
+ {org.name}
+
+ {org.role}
+
+ {active ? (
+
+ ) : null}
+
+ );
+ })}
+
+
+
+ >
+ );
+}
+
+const styles = StyleSheet.create({
+ trigger: {
+ height: 44,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 9,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: colors.gray5,
+ backgroundColor: colors.gray1,
+ borderRadius: radius.full,
+ paddingHorizontal: 10,
+ ...squircle,
+ },
+ triggerPressed: {
+ backgroundColor: colors.gray3,
+ borderColor: colors.gray6,
+ },
+ triggerText: {
+ flex: 1,
+ fontFamily: fonts.medium,
+ color: colors.gray12,
+ fontSize: 15,
+ },
+ avatar: {
+ width: 26,
+ height: 26,
+ borderRadius: radius.xs,
+ overflow: "hidden",
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.blue3,
+ ...squircle,
+ },
+ avatarImage: {
+ width: "100%",
+ height: "100%",
+ },
+ avatarText: {
+ fontFamily: fonts.medium,
+ fontSize: 12,
+ color: colors.blue11,
+ },
+ overlay: {
+ flex: 1,
+ backgroundColor: colors.blackAlpha40,
+ justifyContent: "flex-end",
+ },
+ sheet: {
+ backgroundColor: colors.gray1,
+ borderTopLeftRadius: radius.xl,
+ borderTopRightRadius: radius.xl,
+ padding: 18,
+ paddingBottom: 32,
+ gap: 6,
+ ...shadows.popover,
+ ...squircle,
+ },
+ sheetTitle: {
+ fontFamily: fonts.medium,
+ fontSize: 20,
+ lineHeight: 26,
+ color: colors.gray12,
+ marginBottom: 6,
+ },
+ orgRow: {
+ minHeight: 58,
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 12,
+ borderRadius: radius.sm,
+ paddingHorizontal: 8,
+ ...squircle,
+ },
+ orgTextWrap: {
+ flex: 1,
+ minWidth: 0,
+ },
+ orgName: {
+ fontFamily: fonts.medium,
+ fontSize: 16,
+ color: colors.gray12,
+ },
+ orgRole: {
+ fontFamily: fonts.regular,
+ fontSize: 12,
+ color: colors.gray10,
+ textTransform: "capitalize",
+ },
+});
diff --git a/apps/mobile/src/components/Screen.test.tsx b/apps/mobile/src/components/Screen.test.tsx
new file mode 100644
index 00000000000..0e5fe245882
--- /dev/null
+++ b/apps/mobile/src/components/Screen.test.tsx
@@ -0,0 +1,109 @@
+import type React from "react";
+import TestRenderer, {
+ act,
+ type ReactTestRendererJSON,
+} from "react-test-renderer";
+import { describe, expect, it, vi } from "vitest";
+import { Screen } from "./Screen";
+
+type HostProps = {
+ children?: React.ReactNode;
+ [key: string]: unknown;
+};
+
+type JsonNode = ReactTestRendererJSON | ReactTestRendererJSON[] | string | null;
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const renderTree = async (): Promise => {
+ let renderer: TestRenderer.ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(
+ ,
+ );
+ });
+ return (renderer as TestRenderer.ReactTestRenderer | null)?.toJSON() ?? null;
+};
+
+const findTextByValue = (
+ node: JsonNode,
+ value: string,
+): ReactTestRendererJSON | null => {
+ if (!node || typeof node === "string") return null;
+ if (Array.isArray(node)) {
+ for (const item of node) {
+ const match = findTextByValue(item, value);
+ if (match) return match;
+ }
+ return null;
+ }
+ if (node.type === "Text" && node.children?.includes(value)) return node;
+ for (const child of node.children ?? []) {
+ const match = findTextByValue(child, value);
+ if (match) return match;
+ }
+ return null;
+};
+
+const hasStyle = (
+ node: JsonNode,
+ expected: Record,
+): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node)) return node.some((item) => hasStyle(item, expected));
+ const resolved = Array.isArray(node.props.style)
+ ? Object.assign({}, ...node.props.style.filter(Boolean))
+ : node.props.style;
+ if (
+ resolved &&
+ Object.entries(expected).every(([key, value]) => resolved[key] === value)
+ ) {
+ return true;
+ }
+ return node.children?.some((child) => hasStyle(child, expected)) ?? false;
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ ActivityIndicator: createHost("ActivityIndicator"),
+ RefreshControl: createHost("RefreshControl"),
+ ScrollView: createHost("ScrollView"),
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ },
+ Text: createHost("Text"),
+ View: createHost("View"),
+ };
+});
+
+vi.mock("react-native-safe-area-context", async () => {
+ const React = await import("react");
+ return {
+ SafeAreaView: ({ children, ...props }: HostProps) =>
+ React.createElement("SafeAreaView", props, children),
+ };
+});
+
+describe("Screen", () => {
+ it("uses the Cap web subtitle scale", async () => {
+ const tree = await renderTree();
+ const subtitle = findTextByValue(
+ tree,
+ "Import videos from external sources.",
+ );
+
+ expect(subtitle?.props.style).toMatchObject({
+ fontSize: 14,
+ lineHeight: 20,
+ });
+ expect(hasStyle(tree, { paddingBottom: 32 })).toBe(true);
+ });
+});
diff --git a/apps/mobile/src/components/Screen.tsx b/apps/mobile/src/components/Screen.tsx
new file mode 100644
index 00000000000..2e2e456e8b8
--- /dev/null
+++ b/apps/mobile/src/components/Screen.tsx
@@ -0,0 +1,124 @@
+import type { ReactNode } from "react";
+import {
+ ActivityIndicator,
+ ScrollView,
+ StyleSheet,
+ Text,
+ View,
+} from "react-native";
+import { type Edge, SafeAreaView } from "react-native-safe-area-context";
+import { colors, fonts } from "@/theme";
+import { CapRefreshControl } from "./CapRefreshControl";
+
+type ScreenProps = {
+ children?: ReactNode;
+ title?: string;
+ subtitle?: string | null;
+ scroll?: boolean;
+ refreshing?: boolean;
+ onRefresh?: () => void;
+ loading?: boolean;
+ footer?: ReactNode;
+ safeEdges?: Edge[];
+};
+
+const defaultSafeEdges: Edge[] = ["top", "left", "right"];
+
+export function Screen({
+ children,
+ title,
+ subtitle,
+ scroll = false,
+ refreshing = false,
+ onRefresh,
+ loading = false,
+ footer,
+ safeEdges = defaultSafeEdges,
+}: ScreenProps) {
+ const content = (
+ <>
+ {title ? (
+
+ {title}
+ {subtitle ? {subtitle} : null}
+
+ ) : null}
+ {loading ? (
+
+
+
+ ) : (
+ children
+ )}
+ {footer}
+ >
+ );
+
+ return (
+
+ {scroll ? (
+
+ ) : undefined
+ }
+ >
+ {content}
+
+ ) : (
+ {content}
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ backgroundColor: colors.appBackground,
+ },
+ content: {
+ flex: 1,
+ paddingHorizontal: 20,
+ paddingBottom: 18,
+ },
+ scrollContent: {
+ flexGrow: 1,
+ paddingHorizontal: 20,
+ paddingBottom: 28,
+ },
+ header: {
+ paddingTop: 8,
+ paddingBottom: 16,
+ gap: 4,
+ },
+ headerWithSubtitle: {
+ paddingBottom: 32,
+ },
+ title: {
+ fontFamily: fonts.medium,
+ fontSize: 24,
+ lineHeight: 30,
+ color: colors.gray12,
+ },
+ subtitle: {
+ fontFamily: fonts.regular,
+ fontSize: 14,
+ lineHeight: 20,
+ color: colors.gray10,
+ },
+ loading: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ paddingVertical: 48,
+ },
+});
diff --git a/apps/mobile/src/components/capCardViewModel.ts b/apps/mobile/src/components/capCardViewModel.ts
new file mode 100644
index 00000000000..84aae8189ff
--- /dev/null
+++ b/apps/mobile/src/components/capCardViewModel.ts
@@ -0,0 +1,60 @@
+import type { MobileCapSummary } from "@/api/mobile";
+import { formatDuration, formatRelativeDate } from "../utils/format";
+
+const clampPercent = (value: number) => {
+ const safeValue = Number.isFinite(value) ? value : 0;
+ return Math.min(100, Math.max(0, Math.round(safeValue)));
+};
+
+const getUploadProgress = (cap: MobileCapSummary) => {
+ if (!cap.upload) return null;
+
+ if (cap.upload.phase === "uploading") {
+ return clampPercent(
+ (cap.upload.total > 0 ? cap.upload.uploaded / cap.upload.total : 0) * 100,
+ );
+ }
+
+ return clampPercent(cap.upload.processingProgress);
+};
+
+const getUploadStatusText = (cap: MobileCapSummary) => {
+ if (!cap.upload) return null;
+
+ switch (cap.upload.phase) {
+ case "processing":
+ return cap.upload.processingMessage ?? "Processing";
+ case "generating_thumbnail":
+ return cap.upload.processingMessage ?? "Finishing up";
+ case "complete":
+ return cap.upload.processingMessage ?? "Finishing up";
+ case "error":
+ return cap.upload.processingError ?? "Upload failed";
+ default:
+ return `${getUploadProgress(cap) ?? 0}% uploaded`;
+ }
+};
+
+export const getCapCardViewModel = (
+ cap: MobileCapSummary,
+ now = new Date(),
+) => {
+ const duration = formatDuration(cap.durationSeconds);
+ const date = formatRelativeDate(cap.createdAt, now);
+ const visibility = cap.public ? "Shared" : "Not shared";
+ const uploadStatusText = getUploadStatusText(cap);
+ const uploadProgress = getUploadProgress(cap);
+ const uploadFailed = cap.upload?.phase === "error";
+
+ return {
+ date,
+ duration,
+ visibility,
+ uploadStatusText,
+ uploadProgress,
+ uploadFailed,
+ accessibilityLabel: [cap.title, date, visibility, uploadStatusText]
+ .filter(Boolean)
+ .join(", "),
+ };
+};
diff --git a/apps/mobile/src/screens/account-settings.test.tsx b/apps/mobile/src/screens/account-settings.test.tsx
new file mode 100644
index 00000000000..ad331a8bc2d
--- /dev/null
+++ b/apps/mobile/src/screens/account-settings.test.tsx
@@ -0,0 +1,377 @@
+import React, { type ReactElement, type ReactNode } from "react";
+import TestRenderer, {
+ act,
+ type ReactTestRenderer,
+ type ReactTestRendererJSON,
+} from "react-test-renderer";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import AccountScreen from "../../app/(tabs)/account";
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+type JsonNode = ReactTestRendererJSON | ReactTestRendererJSON[] | string | null;
+
+const auth = vi.hoisted(() => ({
+ value: {
+ status: "signedIn" as const,
+ bootstrap: {
+ activeOrganizationId: "org_123",
+ user: {
+ email: "richie@cap.so",
+ imageUrl: null,
+ name: "Richie",
+ },
+ organizations: [
+ {
+ id: "org_123",
+ iconUrl: null,
+ name: "Cap",
+ role: "owner",
+ },
+ ],
+ rootFolders: [],
+ },
+ refresh: vi.fn(() => Promise.resolve()),
+ setActiveOrganization: vi.fn(() => Promise.resolve()),
+ signOut: vi.fn(() => Promise.resolve()),
+ },
+}));
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const renderComponent = async (
+ node: ReactElement,
+): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+const getTextNodes = (node: JsonNode): string[] => {
+ if (!node) return [];
+ if (typeof node === "string") return [node];
+ if (Array.isArray(node)) return node.flatMap(getTextNodes);
+ return node.children?.flatMap(getTextNodes) ?? [];
+};
+
+const createDeferred = () => {
+ let resolve!: (value: T | PromiseLike) => void;
+ const promise = new Promise((nextResolve) => {
+ resolve = nextResolve;
+ });
+ return { promise, resolve };
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ ActionSheetIOS: {
+ showActionSheetWithOptions: vi.fn(),
+ },
+ ActivityIndicator: createHost("ActivityIndicator"),
+ Alert: {
+ alert: vi.fn(),
+ },
+ Linking: {
+ openSettings: vi.fn(),
+ },
+ Platform: {
+ OS: "ios",
+ },
+ Pressable: createHost("Pressable"),
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ hairlineWidth: 1,
+ },
+ Text: createHost("Text"),
+ View: createHost("View"),
+ };
+});
+
+vi.mock("expo-constants", () => ({
+ default: {
+ expoConfig: {
+ version: "0.1.0",
+ },
+ },
+}));
+
+vi.mock("expo-image", async () => {
+ const React = await import("react");
+ return {
+ Image: (props: Record) =>
+ React.createElement("Image", props),
+ };
+});
+
+vi.mock("expo-symbols", async () => {
+ const React = await import("react");
+ return {
+ SymbolView: (props: Record) =>
+ React.createElement("SymbolView", props),
+ };
+});
+
+vi.mock("expo-web-browser", () => ({
+ openBrowserAsync: vi.fn(),
+}));
+
+vi.mock("@/auth/AuthContext", () => ({
+ apiBaseUrl: "https://cap.so",
+ useAuth: () => auth.value,
+}));
+
+vi.mock("@/auth/SignInPanel", async () => {
+ const React = await import("react");
+ return {
+ SignInPanel: () => React.createElement("SignInPanel"),
+ };
+});
+
+vi.mock("@/components/GlassSurface", async () => {
+ const React = await import("react");
+ return {
+ GlassSurface: ({ children }: { children?: ReactNode }) =>
+ React.createElement("GlassSurface", null, children),
+ };
+});
+
+vi.mock("@/components/OrgSwitcher", async () => {
+ const React = await import("react");
+ return {
+ OrgSwitcher: () => React.createElement("OrgSwitcher"),
+ };
+});
+
+vi.mock("@/components/Screen", async () => {
+ const React = await import("react");
+ return {
+ Screen: ({
+ children,
+ subtitle,
+ title,
+ }: {
+ children?: ReactNode;
+ subtitle?: string | null;
+ title?: string;
+ }) =>
+ React.createElement(
+ "Screen",
+ null,
+ title ? React.createElement("Text", null, title) : null,
+ subtitle ? React.createElement("Text", null, subtitle) : null,
+ children,
+ ),
+ };
+});
+
+describe("AccountScreen", () => {
+ beforeEach(() => {
+ auth.value.refresh.mockReset();
+ auth.value.refresh.mockResolvedValue(undefined);
+ auth.value.setActiveOrganization.mockReset();
+ auth.value.setActiveOrganization.mockResolvedValue(undefined);
+ auth.value.signOut.mockReset();
+ auth.value.signOut.mockResolvedValue(undefined);
+ });
+
+ it("opens organization settings in the native browser sheet", async () => {
+ const renderer = await renderComponent(React.createElement(AccountScreen));
+ const text = getTextNodes(renderer.toJSON());
+ const [organizationSettings] = renderer.root.findAllByProps({
+ accessibilityLabel: "Organization Settings",
+ });
+ if (!organizationSettings) {
+ throw new Error("Organization Settings row was not rendered");
+ }
+
+ expect(text).toContain("Account");
+ expect(text).toContain("Organization Settings");
+ expect(organizationSettings.props.accessibilityHint).toBe(
+ "Opens organization settings in a browser sheet",
+ );
+
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ const openDeferred =
+ createDeferred>>();
+ openBrowserAsync.mockClear();
+ openBrowserAsync.mockReturnValueOnce(openDeferred.promise);
+
+ await act(async () => {
+ organizationSettings.props.onPress();
+ await Promise.resolve();
+ });
+
+ expect(openBrowserAsync).toHaveBeenCalledWith(
+ "https://cap.so/dashboard/settings/organization",
+ );
+ const [openingOrganizationSettings] = renderer.root.findAllByProps({
+ accessibilityLabel: "Organization Settings",
+ });
+ expect(openingOrganizationSettings?.props.accessibilityValue).toEqual({
+ text: "Opening organization settings",
+ });
+
+ await act(async () => {
+ openDeferred.resolve({
+ type: "dismiss",
+ } as Awaited>);
+ await openDeferred.promise;
+ });
+ });
+
+ it("marks app settings as opening with a native value", async () => {
+ const renderer = await renderComponent(React.createElement(AccountScreen));
+ const [appSettings] = renderer.root.findAllByProps({
+ accessibilityLabel: "App Settings",
+ });
+ if (!appSettings) throw new Error("App Settings row was not rendered");
+
+ const { Linking } = await import("react-native");
+ const openSettings = vi.mocked(Linking.openSettings);
+ const openDeferred = createDeferred();
+ openSettings.mockClear();
+ openSettings.mockReturnValueOnce(openDeferred.promise);
+
+ await act(async () => {
+ appSettings.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [openingAppSettings] = renderer.root.findAllByProps({
+ accessibilityLabel: "App Settings",
+ });
+ expect(openingAppSettings?.props.accessibilityValue).toEqual({
+ text: "Opening iOS app settings",
+ });
+
+ await act(async () => {
+ openDeferred.resolve();
+ await openDeferred.promise;
+ });
+ });
+
+ it("locks account settings rows while refresh is in progress", async () => {
+ const refreshDeferred = createDeferred();
+ auth.value.refresh.mockReturnValueOnce(refreshDeferred.promise);
+ const renderer = await renderComponent(React.createElement(AccountScreen));
+ const [refreshRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "Refresh",
+ });
+ if (!refreshRow) throw new Error("Refresh row was not rendered");
+
+ await act(async () => {
+ refreshRow.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [loadingRefreshRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "Refresh",
+ });
+ const [organizationSettings] = renderer.root.findAllByProps({
+ accessibilityLabel: "Organization Settings",
+ });
+ const [appSettings] = renderer.root.findAllByProps({
+ accessibilityLabel: "App Settings",
+ });
+ const [signOut] = renderer.root.findAllByProps({
+ accessibilityLabel: "Sign out",
+ });
+ if (
+ !loadingRefreshRow ||
+ !organizationSettings ||
+ !appSettings ||
+ !signOut
+ ) {
+ throw new Error("Account action rows were not rendered");
+ }
+
+ expect(loadingRefreshRow.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(loadingRefreshRow.props.accessibilityHint).toBe(
+ "Refresh is in progress",
+ );
+ expect(loadingRefreshRow.props.accessibilityValue).toEqual({
+ text: "Refreshing account data",
+ });
+ expect(getTextNodes(renderer.toJSON())).toContain("Refreshing...");
+ for (const row of [organizationSettings, appSettings, signOut]) {
+ expect(row.props.disabled).toBe(true);
+ expect(row.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: true,
+ });
+ expect(row.props.accessibilityHint).toBe("Refresh is in progress");
+ }
+
+ await act(async () => {
+ refreshDeferred.resolve();
+ await refreshDeferred.promise;
+ });
+ });
+
+ it("shows sign-out as busy after confirmation", async () => {
+ const signOutDeferred = createDeferred();
+ auth.value.signOut.mockReturnValueOnce(signOutDeferred.promise);
+ const renderer = await renderComponent(React.createElement(AccountScreen));
+ const [signOut] = renderer.root.findAllByProps({
+ accessibilityLabel: "Sign out",
+ });
+ if (!signOut) throw new Error("Sign out row was not rendered");
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ signOut.props.onPress();
+ });
+
+ const [, callback] = showActionSheetWithOptions.mock.calls[0] ?? [];
+ if (!callback)
+ throw new Error("Sign-out confirmation callback was not set");
+
+ await act(async () => {
+ callback(0);
+ await Promise.resolve();
+ });
+
+ const [loadingSignOut] = renderer.root.findAllByProps({
+ accessibilityLabel: "Sign out",
+ });
+ if (!loadingSignOut) throw new Error("Sign out row was not rendered");
+ expect(loadingSignOut.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(loadingSignOut.props.accessibilityHint).toBe(
+ "Sign out is in progress",
+ );
+ expect(loadingSignOut.props.accessibilityValue).toEqual({
+ text: "Signing out of Cap",
+ });
+ expect(getTextNodes(renderer.toJSON())).toContain("Signing out...");
+
+ await act(async () => {
+ signOutDeferred.resolve();
+ await signOutDeferred.promise;
+ });
+ });
+});
diff --git a/apps/mobile/src/screens/cap-detail.test.tsx b/apps/mobile/src/screens/cap-detail.test.tsx
new file mode 100644
index 00000000000..c987b6fae80
--- /dev/null
+++ b/apps/mobile/src/screens/cap-detail.test.tsx
@@ -0,0 +1,671 @@
+import { Comment, User, Video } from "@cap/web-domain";
+import React, { type ReactElement, type ReactNode } from "react";
+import TestRenderer, {
+ act,
+ type ReactTestRenderer,
+ type ReactTestRendererJSON,
+} from "react-test-renderer";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import type {
+ MobileCapDetail,
+ MobileComment,
+ MobilePlaybackResponse,
+} from "@/api/mobile";
+import CapDetailScreen from "../../app/caps/[id]";
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+type JsonNode = ReactTestRendererJSON | ReactTestRendererJSON[] | string | null;
+
+type AuthStub = {
+ status: "signedIn";
+ client: {
+ createComment: ReturnType;
+ createReaction: ReturnType;
+ deleteCap: ReturnType;
+ getCap: ReturnType;
+ getPlayback: ReturnType;
+ updateCapSharing: ReturnType;
+ };
+ refresh: ReturnType;
+};
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const detail: MobileCapDetail = {
+ cap: {
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: 125,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: true,
+ viewCount: 17,
+ commentCount: 2,
+ reactionCount: 3,
+ upload: null,
+ },
+ summary: "A short launch walkthrough.",
+ chapters: [],
+ transcriptionStatus: "COMPLETE",
+ comments: [],
+ shareUrl: "https://cap.so/s/video_123",
+};
+
+const playback: MobilePlaybackResponse = {
+ kind: "mp4",
+ transcriptUrl: null,
+ url: "https://cap.so/video.mp4",
+};
+
+const createdComment = (content: string): MobileComment => ({
+ id: Comment.CommentId.make("comment_123"),
+ videoId: Video.VideoId.make("video_123"),
+ type: "text",
+ content,
+ timestamp: null,
+ parentCommentId: null,
+ createdAt: "2026-05-18T10:31:00.000Z",
+ updatedAt: "2026-05-18T10:31:00.000Z",
+ author: {
+ id: User.UserId.make("user_123"),
+ name: "Richie",
+ imageUrl: null,
+ },
+});
+
+const createDeferred = () => {
+ let resolve!: (value: T | PromiseLike) => void;
+ const promise = new Promise((nextResolve) => {
+ resolve = nextResolve;
+ });
+ return { promise, resolve };
+};
+
+const authState = vi.hoisted((): { value: AuthStub | null } => ({
+ value: null,
+}));
+
+const renderComponent = async (
+ node: ReactElement,
+): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+const getTextNodes = (node: JsonNode): string[] => {
+ if (!node) return [];
+ if (typeof node === "string") return [node];
+ if (Array.isArray(node)) return node.flatMap(getTextNodes);
+ return node.children?.flatMap(getTextNodes) ?? [];
+};
+
+const hasProp = (node: JsonNode, prop: string, value: unknown): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node))
+ return node.some((item) => hasProp(item, prop, value));
+ if (node.props[prop] === value) return true;
+ return node.children?.some((child) => hasProp(child, prop, value)) ?? false;
+};
+
+const resolveStyle = (
+ style: unknown,
+ pressed = false,
+): Record => {
+ const resolved =
+ typeof style === "function"
+ ? (style as (state: { pressed: boolean }) => unknown)({ pressed })
+ : style;
+ const styles = Array.isArray(resolved) ? resolved : [resolved];
+ return Object.assign({}, ...styles.filter(Boolean));
+};
+
+const createAuth = (): AuthStub => ({
+ status: "signedIn",
+ client: {
+ createComment: vi.fn(),
+ createReaction: vi.fn(),
+ deleteCap: vi.fn(),
+ getCap: vi.fn(() => Promise.resolve(detail)),
+ getPlayback: vi.fn(() => Promise.resolve(playback)),
+ updateCapSharing: vi.fn(),
+ },
+ refresh: vi.fn(() => Promise.resolve()),
+});
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ ActionSheetIOS: {
+ showActionSheetWithOptions: vi.fn(),
+ },
+ Alert: {
+ alert: vi.fn(),
+ },
+ KeyboardAvoidingView: createHost("KeyboardAvoidingView"),
+ Linking: {
+ openSettings: vi.fn(),
+ },
+ Platform: {
+ OS: "ios",
+ },
+ Pressable: createHost("Pressable"),
+ Share: {
+ share: vi.fn(),
+ },
+ StyleSheet: {
+ absoluteFillObject: {
+ bottom: 0,
+ left: 0,
+ position: "absolute",
+ right: 0,
+ top: 0,
+ },
+ create: >(styles: T) => styles,
+ hairlineWidth: 1,
+ },
+ Text: createHost("Text"),
+ TextInput: createHost("TextInput"),
+ View: createHost("View"),
+ };
+});
+
+vi.mock("expo-clipboard", () => ({
+ setStringAsync: vi.fn(),
+}));
+
+vi.mock("expo-router", async () => {
+ const React = await import("react");
+ return {
+ router: {
+ back: vi.fn(),
+ },
+ Stack: {
+ Screen: (props: HostProps) =>
+ React.createElement("StackScreen", {
+ ...props,
+ testID: "stack-screen",
+ }),
+ },
+ useLocalSearchParams: () => ({ id: "video_123" }),
+ };
+});
+
+vi.mock("expo-symbols", async () => {
+ const React = await import("react");
+ return {
+ SymbolView: (props: Record) =>
+ React.createElement("SymbolView", props),
+ };
+});
+
+vi.mock("expo-video", async () => {
+ const React = await import("react");
+ return {
+ VideoView: (props: Record) =>
+ React.createElement("VideoView", props),
+ useVideoPlayer: () => ({
+ replace: vi.fn(),
+ }),
+ };
+});
+
+vi.mock("expo-web-browser", () => ({
+ openBrowserAsync: vi.fn(),
+}));
+
+vi.mock("@/auth/AuthContext", () => ({
+ apiBaseUrl: "https://cap.so",
+ useAuth: () => authState.value,
+}));
+
+vi.mock("@/auth/SignInPanel", async () => {
+ const React = await import("react");
+ return {
+ SignInPanel: () => React.createElement("SignInPanel"),
+ };
+});
+
+vi.mock("@/caps/CapSettingsSheet", async () => {
+ const React = await import("react");
+ return {
+ CapSettingsSheet: (props: Record) =>
+ React.createElement("CapSettingsSheet", {
+ ...props,
+ testID: "cap-settings-sheet",
+ }),
+ };
+});
+
+vi.mock("@/caps/passwordActions", () => ({
+ showCapPasswordActions: vi.fn(),
+}));
+
+vi.mock("@/caps/saveCapVideo", () => ({
+ PhotosPermissionDeniedError: class PhotosPermissionDeniedError extends Error {},
+ saveCapVideoToPhotos: vi.fn(),
+}));
+
+vi.mock("@/caps/titleActions", () => ({
+ showCapTitleActions: vi.fn(),
+}));
+
+vi.mock("@/components/ActionButton", async () => {
+ const React = await import("react");
+ return {
+ ActionButton: ({
+ children,
+ label,
+ onPress,
+ ...props
+ }: {
+ children?: ReactNode;
+ label: string;
+ onPress?: () => void;
+ [key: string]: unknown;
+ }) =>
+ React.createElement(
+ "ActionButton",
+ {
+ ...props,
+ accessibilityLabel: props.accessibilityLabel ?? label,
+ onPress,
+ },
+ children ?? label,
+ ),
+ };
+});
+
+vi.mock("@/components/GlassSurface", async () => {
+ const React = await import("react");
+ return {
+ GlassSurface: ({ children }: { children?: ReactNode }) =>
+ React.createElement("GlassSurface", null, children),
+ };
+});
+
+vi.mock("@/components/Screen", async () => {
+ const React = await import("react");
+ return {
+ Screen: ({
+ children,
+ loading,
+ }: {
+ children?: ReactNode;
+ loading?: boolean;
+ }) =>
+ React.createElement(
+ "Screen",
+ null,
+ loading ? React.createElement("Text", null, "Loading") : children,
+ ),
+ };
+});
+
+describe("Cap detail screen", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ authState.value = createAuth();
+ });
+
+ it("announces Cap detail load errors with a retry action", async () => {
+ const auth = createAuth();
+ auth.client.getCap = vi.fn(() =>
+ Promise.reject(new Error("Network unavailable")),
+ );
+ authState.value = auth;
+
+ const renderer = await renderComponent(
+ React.createElement(CapDetailScreen),
+ );
+ const tree = renderer.toJSON();
+ const text = getTextNodes(tree);
+
+ expect(text).toContain("Unable to load Cap");
+ expect(text).toContain("Network unavailable");
+ expect(hasProp(tree, "accessibilityRole", "alert")).toBe(true);
+ expect(hasProp(tree, "accessibilityLiveRegion", "polite")).toBe(true);
+ expect(
+ hasProp(
+ tree,
+ "accessibilityLabel",
+ "Cap detail error: Network unavailable",
+ ),
+ ).toBe(true);
+
+ const [retryButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Try again",
+ });
+ if (!retryButton)
+ throw new Error("Cap detail retry action was not rendered");
+ expect(retryButton.props.accessibilityHint).toBe("Reloads this Cap");
+
+ await act(async () => {
+ retryButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ expect(auth.client.getCap).toHaveBeenCalledTimes(2);
+ });
+
+ it("shows web-matching sharing, analytics, and action labels", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapDetailScreen),
+ );
+ const tree = renderer.toJSON();
+ const text = getTextNodes(tree);
+
+ expect(text).toContain("Launch review");
+ expect(text).toContain("Shared");
+ expect(text).toContain("Password protected");
+ expect(text).toContain("17");
+ expect(text).toContain("2");
+ expect(text).toContain("3");
+ expect(text).toContain("Copy link");
+ expect(text).toContain("Save video");
+ expect(text).toContain("View analytics");
+ expect(hasProp(tree, "accessibilityHint", "Copies this Cap link")).toBe(
+ true,
+ );
+ expect(
+ hasProp(tree, "accessibilityHint", "Opens the native share sheet"),
+ ).toBe(true);
+ expect(
+ hasProp(tree, "accessibilityHint", "Saves this video to Photos"),
+ ).toBe(true);
+ expect(
+ hasProp(tree, "accessibilityLabel", "Change sharing for Launch review"),
+ ).toBe(true);
+ expect(hasProp(tree, "accessibilityHint", "Opens sharing settings")).toBe(
+ true,
+ );
+ expect(
+ hasProp(tree, "accessibilityLabel", "View analytics for Launch review"),
+ ).toBe(true);
+ expect(
+ hasProp(tree, "accessibilityHint", "Opens analytics in a browser sheet"),
+ ).toBe(true);
+ });
+
+ it("uses native affordances for the header menu and comment composer", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapDetailScreen),
+ );
+ const stackScreen = renderer.root.findByProps({
+ testID: "stack-screen",
+ });
+ const headerRight = stackScreen.props.options
+ .headerRight as () => ReactNode;
+ let headerRenderer: ReactTestRenderer | null = null;
+
+ await act(async () => {
+ headerRenderer = TestRenderer.create(headerRight() as ReactElement);
+ });
+
+ const headerAction = (
+ headerRenderer as unknown as ReactTestRenderer
+ ).root.findByProps({
+ accessibilityLabel: "More actions",
+ });
+ expect(headerAction.props.accessibilityState).toEqual({
+ disabled: false,
+ });
+ expect(headerAction.props.accessibilityHint).toBe("Opens Cap settings");
+ expect(headerAction.props.hitSlop).toBe(10);
+ expect(resolveStyle(headerAction.props.style, true)).toMatchObject({
+ backgroundColor: "#f0f0f0",
+ });
+
+ const [commentInput] = renderer.root.findAllByProps({
+ accessibilityLabel: "Comment",
+ });
+ if (!commentInput) throw new Error("Comment input was not rendered");
+
+ expect(commentInput.props.accessibilityState).toEqual({
+ disabled: false,
+ });
+ expect(commentInput.props.enablesReturnKeyAutomatically).toBe(true);
+ expect(commentInput.props.keyboardAppearance).toBe("light");
+ expect(commentInput.props.returnKeyType).toBe("send");
+ expect(commentInput.props.selectionColor).toBe("#0d74ce");
+ expect(commentInput.props.submitBehavior).toBe("blurAndSubmit");
+
+ const [disabledSend] = renderer.root.findAllByProps({
+ accessibilityLabel: "Send comment",
+ });
+ expect(disabledSend?.props.disabled).toBe(true);
+
+ await act(async () => {
+ commentInput.props.onChangeText("Ship it");
+ });
+
+ const [enabledSend] = renderer.root.findAllByProps({
+ accessibilityLabel: "Send comment",
+ });
+ expect(enabledSend?.props.disabled).toBe(false);
+ });
+
+ it("opens native settings from the sharing status", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapDetailScreen),
+ );
+ const [shareStatus] = renderer.root.findAllByProps({
+ accessibilityLabel: "Change sharing for Launch review",
+ });
+ if (!shareStatus) throw new Error("Sharing status row was not rendered");
+
+ await act(async () => {
+ shareStatus.props.onPress();
+ });
+
+ const [sheet] = renderer.root.findAllByProps({
+ testID: "cap-settings-sheet",
+ });
+ expect(sheet?.props.visible).toBe(true);
+ });
+
+ it("opens analytics in the native browser sheet", async () => {
+ const renderer = await renderComponent(
+ React.createElement(CapDetailScreen),
+ );
+ const [analytics] = renderer.root.findAllByProps({
+ accessibilityLabel: "View analytics for Launch review",
+ });
+ if (!analytics) throw new Error("Analytics row was not rendered");
+
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ openBrowserAsync.mockClear();
+
+ await act(async () => {
+ analytics.props.onPress();
+ });
+
+ expect(openBrowserAsync).toHaveBeenCalledWith(
+ "https://cap.so/dashboard/analytics?capId=video_123",
+ );
+ });
+
+ it("shows a save-specific busy state without blocking sharing as saving", async () => {
+ const saveDeferred = createDeferred();
+ const { saveCapVideoToPhotos } = await import("@/caps/saveCapVideo");
+ vi.mocked(saveCapVideoToPhotos).mockReturnValueOnce(saveDeferred.promise);
+ const renderer = await renderComponent(
+ React.createElement(CapDetailScreen),
+ );
+ const [saveButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Save video",
+ });
+ if (!saveButton) throw new Error("Save video button was not rendered");
+
+ await act(async () => {
+ saveButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [savingButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Save video",
+ });
+ if (!savingButton) throw new Error("Saving button was not rendered");
+ expect(savingButton.props.loading).toBe(true);
+ expect(savingButton.props.accessibilityHint).toBe("Save is in progress");
+ expect(savingButton.props.accessibilityValue).toEqual({
+ text: "Saving video for Launch review",
+ });
+
+ const [sheet] = renderer.root.findAllByProps({
+ testID: "cap-settings-sheet",
+ });
+ expect(sheet?.props.saveDisabled).toBe(true);
+ expect(sheet?.props.saveDisabledHint).toBe("Save is in progress");
+ expect(sheet?.props.saveDisabledValue).toBeUndefined();
+ expect(sheet?.props.saveDisabledAccessibilityValue).toBe(
+ "Saving video for Launch review",
+ );
+ expect(sheet?.props.visibilityDisabled).toBe(true);
+ expect(sheet?.props.visibilityDisabledHint).toBe(
+ "Current Cap action is in progress",
+ );
+ expect(sheet?.props.visibilityDisabledValue).toBeUndefined();
+
+ await act(async () => {
+ saveDeferred.resolve("Launch review.mp4");
+ await saveDeferred.promise;
+ });
+
+ expect(getTextNodes(renderer.toJSON())).toContain("Saved");
+ });
+
+ it("keeps save idle while a comment is sending", async () => {
+ const commentDeferred = createDeferred();
+ const auth = createAuth();
+ auth.client.createComment.mockReturnValueOnce(commentDeferred.promise);
+ authState.value = auth;
+ const renderer = await renderComponent(
+ React.createElement(CapDetailScreen),
+ );
+ const [commentInput] = renderer.root.findAllByProps({
+ accessibilityLabel: "Comment",
+ });
+ if (!commentInput) throw new Error("Comment input was not rendered");
+
+ await act(async () => {
+ commentInput.props.onChangeText("Ship it");
+ });
+
+ const [sendButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Send comment",
+ });
+ if (!sendButton) throw new Error("Send button was not rendered");
+
+ await act(async () => {
+ sendButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [sendingButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Sending comment on Launch review",
+ });
+ const [saveButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Save video",
+ });
+ if (!sendingButton) throw new Error("Sending button was not rendered");
+ if (!saveButton) throw new Error("Save video button was not rendered");
+
+ expect(sendingButton.props.loading).toBe(true);
+ expect(sendingButton.props.accessibilityHint).toBe("Comment is being sent");
+ expect(saveButton.props.loading).toBe(false);
+ expect(saveButton.props.disabled).toBe(true);
+ expect(saveButton.props.accessibilityHint).toBe(
+ "Current Cap action is in progress",
+ );
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Saving...");
+
+ const [sheet] = renderer.root.findAllByProps({
+ testID: "cap-settings-sheet",
+ });
+ expect(sheet?.props.saveDisabledValue).toBe("Unavailable");
+ expect(sheet?.props.visibilityDisabledValue).toBeUndefined();
+
+ await act(async () => {
+ commentDeferred.resolve(createdComment("Ship it"));
+ await commentDeferred.promise;
+ });
+
+ expect(getTextNodes(renderer.toJSON())).toContain("Ship it");
+ });
+
+ it("marks the settings sheet sharing row as updating during visibility changes", async () => {
+ const sharingDeferred = createDeferred();
+ const auth = createAuth();
+ auth.client.updateCapSharing.mockReturnValueOnce(sharingDeferred.promise);
+ authState.value = auth;
+ const renderer = await renderComponent(
+ React.createElement(CapDetailScreen),
+ );
+ const [sheet] = renderer.root.findAllByProps({
+ testID: "cap-settings-sheet",
+ });
+ if (!sheet) throw new Error("Cap settings sheet was not rendered");
+
+ await act(async () => {
+ sheet.props.onVisibilityChange(detail.cap, false);
+ await Promise.resolve();
+ });
+
+ const [busySheet] = renderer.root.findAllByProps({
+ testID: "cap-settings-sheet",
+ });
+ const [sharingStatus] = renderer.root.findAllByProps({
+ accessibilityLabel: "Change sharing for Launch review",
+ });
+ expect(auth.client.updateCapSharing).toHaveBeenCalledWith("video_123", {
+ public: false,
+ });
+ expect(sharingStatus?.props.accessibilityHint).toBe(
+ "Sharing update is in progress",
+ );
+ expect(sharingStatus?.props.accessibilityState).toEqual({
+ disabled: true,
+ });
+ expect(sharingStatus?.props.accessibilityValue).toEqual({
+ text: "Updating sharing for Launch review",
+ });
+ expect(getTextNodes(renderer.toJSON())).toContain("Shared");
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Updating...");
+ expect(busySheet?.props.visibilityDisabled).toBe(true);
+ expect(busySheet?.props.visibilityDisabledHint).toBe(
+ "Sharing update is in progress",
+ );
+ expect(busySheet?.props.visibilityDisabledValue).toBeUndefined();
+ expect(busySheet?.props.visibilityDisabledAccessibilityValue).toBe(
+ "Updating sharing for Launch review",
+ );
+ expect(busySheet?.props.saveDisabledValue).toBe("Unavailable");
+
+ await act(async () => {
+ sharingDeferred.resolve({ ...detail.cap, public: false });
+ await sharingDeferred.promise;
+ });
+ });
+});
diff --git a/apps/mobile/src/screens/dashboard-upload-visibility.test.tsx b/apps/mobile/src/screens/dashboard-upload-visibility.test.tsx
new file mode 100644
index 00000000000..b0880df9d60
--- /dev/null
+++ b/apps/mobile/src/screens/dashboard-upload-visibility.test.tsx
@@ -0,0 +1,2377 @@
+import React, { type ReactElement, type ReactNode } from "react";
+import TestRenderer, {
+ act,
+ type ReactTestRenderer,
+ type ReactTestRendererJSON,
+} from "react-test-renderer";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import CapsScreen from "../../app/(tabs)";
+import UploadScreen from "../../app/(tabs)/upload";
+
+type AuthStub = {
+ status: "signedIn";
+ bootstrap: {
+ activeOrganizationId: string;
+ user: {
+ email: string;
+ name: string | null;
+ };
+ };
+ client: {
+ createFolder: (input: { color?: string; name: string }) => Promise<{
+ color: string;
+ id: string;
+ name: string;
+ parentId: null;
+ videoCount: number;
+ }>;
+ getCap: (id: string) => Promise<{
+ cap: {
+ upload: null;
+ };
+ }>;
+ listCaps: () => Promise<{
+ caps: unknown[];
+ folders: unknown[];
+ pagination: {
+ hasNextPage: boolean;
+ page: number;
+ totalPages: number;
+ };
+ rootFolders: unknown[];
+ }>;
+ updateCapSharing: (
+ id: string,
+ input: { public: boolean },
+ ) => Promise;
+ };
+ refresh: () => Promise;
+};
+
+type HostProps = {
+ children?: ReactNode;
+ [key: string]: unknown;
+};
+
+type JsonNode = ReactTestRendererJSON | ReactTestRendererJSON[] | string | null;
+
+const createAuth = (): AuthStub => ({
+ status: "signedIn",
+ bootstrap: {
+ activeOrganizationId: "org_123",
+ user: {
+ email: "richie@cap.so",
+ name: "Richie",
+ },
+ },
+ client: {
+ createFolder: vi.fn((input: { color?: string; name: string }) =>
+ Promise.resolve({
+ id: "folder_123",
+ name: input.name,
+ color: input.color ?? "normal",
+ parentId: null,
+ videoCount: 0,
+ }),
+ ),
+ getCap: () =>
+ Promise.resolve({
+ cap: {
+ upload: null,
+ },
+ }),
+ listCaps: () =>
+ Promise.resolve({
+ caps: [],
+ folders: [],
+ pagination: {
+ hasNextPage: false,
+ page: 1,
+ totalPages: 1,
+ },
+ rootFolders: [],
+ }),
+ updateCapSharing: vi.fn((id: string, input: { public: boolean }) =>
+ Promise.resolve({
+ id,
+ public: input.public,
+ }),
+ ),
+ },
+ refresh: () => Promise.resolve(),
+});
+
+const createDeferred = () => {
+ let resolve!: (value: T | PromiseLike) => void;
+ const promise = new Promise((nextResolve) => {
+ resolve = nextResolve;
+ });
+ return { promise, resolve };
+};
+
+const authState = vi.hoisted((): { value: AuthStub | null } => ({
+ value: null,
+}));
+
+const uploadQueueState = vi.hoisted(
+ (): {
+ value: {
+ items: Array<{
+ capId: string | null;
+ contentType: string;
+ createdAt: string;
+ error: string | null;
+ fileName: string;
+ folderId: string | null;
+ id: string;
+ localUri: string;
+ organizationId: string | null;
+ progress: number;
+ processingMessage?: string | null;
+ rawFileKey: string | null;
+ size: number;
+ durationSeconds?: number;
+ status: "complete" | "failed" | "processing" | "queued" | "uploading";
+ updatedAt: string;
+ }>;
+ };
+ } => ({
+ value: {
+ items: [],
+ },
+ }),
+);
+
+const uploadQueueActionsState = vi.hoisted((): { value: unknown[] } => ({
+ value: [],
+}));
+
+(
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const renderTree = async (node: ReactElement): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return (renderer as ReactTestRenderer | null)?.toJSON() ?? null;
+};
+
+const renderComponent = async (
+ node: ReactElement,
+): Promise => {
+ let renderer: ReactTestRenderer | null = null;
+ await act(async () => {
+ renderer = TestRenderer.create(node);
+ });
+ return renderer as unknown as ReactTestRenderer;
+};
+
+const getTextNodes = (node: JsonNode): string[] => {
+ if (!node) return [];
+ if (typeof node === "string") return [node];
+ if (Array.isArray(node)) return node.flatMap(getTextNodes);
+ return node.children?.flatMap(getTextNodes) ?? [];
+};
+
+const hasProp = (node: JsonNode, prop: string, value: unknown): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node))
+ return node.some((item) => hasProp(item, prop, value));
+ if (node.props[prop] === value) return true;
+ return node.children?.some((child) => hasProp(child, prop, value)) ?? false;
+};
+
+const propMatches = (actual: unknown, expected: unknown): boolean => {
+ if (
+ expected &&
+ typeof expected === "object" &&
+ !Array.isArray(expected) &&
+ actual &&
+ typeof actual === "object" &&
+ !Array.isArray(actual)
+ ) {
+ return Object.entries(expected).every(
+ ([key, value]) => (actual as Record)[key] === value,
+ );
+ }
+
+ return actual === expected;
+};
+
+const hasProps = (
+ node: JsonNode,
+ expected: Record,
+): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node)) return node.some((item) => hasProps(item, expected));
+ if (
+ Object.entries(expected).every(([key, value]) =>
+ propMatches(node.props[key], value),
+ )
+ ) {
+ return true;
+ }
+ return node.children?.some((child) => hasProps(child, expected)) ?? false;
+};
+
+const hasStyle = (
+ node: JsonNode,
+ expected: Record,
+): boolean => {
+ if (!node || typeof node === "string") return false;
+ if (Array.isArray(node)) return node.some((item) => hasStyle(item, expected));
+ const style =
+ typeof node.props.style === "function"
+ ? node.props.style({ pressed: false })
+ : node.props.style;
+ const resolved = Array.isArray(style)
+ ? Object.assign({}, ...style.filter(Boolean))
+ : style;
+ if (
+ resolved &&
+ Object.entries(expected).every(([key, value]) => resolved[key] === value)
+ ) {
+ return true;
+ }
+ return node.children?.some((child) => hasStyle(child, expected)) ?? false;
+};
+
+const resolveStyle = (
+ style: unknown,
+ pressed = false,
+): Record => {
+ const resolved = typeof style === "function" ? style({ pressed }) : style;
+ const styles = Array.isArray(resolved) ? resolved : [resolved];
+ return Object.assign({}, ...styles.filter(Boolean));
+};
+
+vi.mock("react-native", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ ActionSheetIOS: {
+ showActionSheetWithOptions: vi.fn(),
+ },
+ ActivityIndicator: createHost("ActivityIndicator"),
+ Alert: {
+ alert: vi.fn(),
+ prompt: vi.fn(),
+ },
+ AppState: {
+ addEventListener: vi.fn(() => ({
+ remove: vi.fn(),
+ })),
+ },
+ Linking: {
+ openSettings: vi.fn(),
+ },
+ Modal: createHost("Modal"),
+ Platform: {
+ OS: "ios",
+ select: (values: { default?: T; ios?: T }) =>
+ values.ios ?? values.default,
+ },
+ Pressable: createHost("Pressable"),
+ RefreshControl: createHost("RefreshControl"),
+ Share: {
+ share: vi.fn(),
+ },
+ StyleSheet: {
+ absoluteFillObject: {
+ bottom: 0,
+ left: 0,
+ position: "absolute",
+ right: 0,
+ top: 0,
+ },
+ create: >(styles: T) => styles,
+ hairlineWidth: 1,
+ },
+ Switch: createHost("Switch"),
+ Text: createHost("Text"),
+ TextInput: createHost("TextInput"),
+ View: createHost("View"),
+ };
+});
+
+vi.mock("@shopify/flash-list", async () => {
+ const React = await import("react");
+ return {
+ FlashList: ({
+ data,
+ ListEmptyComponent,
+ renderItem,
+ }: {
+ data?: unknown[];
+ ListEmptyComponent?: ReactNode;
+ renderItem?: (info: { index: number; item: unknown }) => ReactNode;
+ }) =>
+ React.createElement(
+ "FlashList",
+ null,
+ data && data.length > 0
+ ? data.map((item, index) =>
+ React.createElement(
+ React.Fragment,
+ { key: index },
+ renderItem?.({ item, index }),
+ ),
+ )
+ : ListEmptyComponent,
+ ),
+ };
+});
+
+vi.mock("expo-clipboard", () => ({
+ setStringAsync: vi.fn(),
+}));
+
+vi.mock("expo-router", () => ({
+ router: {
+ push: vi.fn(),
+ },
+}));
+
+vi.mock("expo-web-browser", () => ({
+ openBrowserAsync: vi.fn(),
+}));
+
+vi.mock("@/auth/AuthContext", () => ({
+ apiBaseUrl: "https://cap.so",
+ useAuth: () => authState.value,
+}));
+
+vi.mock("@/auth/SignInPanel", async () => {
+ const React = await import("react");
+ return {
+ SignInPanel: () => React.createElement("SignInPanel"),
+ };
+});
+
+vi.mock("@/components/ActionButton", async () => {
+ const React = await import("react");
+ return {
+ ActionButton: ({
+ children,
+ label,
+ onPress,
+ ...props
+ }: {
+ children?: ReactNode;
+ label: string;
+ onPress?: () => void;
+ [key: string]: unknown;
+ }) =>
+ React.createElement(
+ "ActionButton",
+ { accessibilityLabel: label, onPress, ...props },
+ children ?? label,
+ ),
+ };
+});
+
+vi.mock("@/components/Screen", async () => {
+ const React = await import("react");
+
+ return {
+ Screen: ({
+ children,
+ loading,
+ subtitle,
+ title,
+ }: {
+ children?: ReactNode;
+ loading?: boolean;
+ subtitle?: string | null;
+ title?: string;
+ }) =>
+ React.createElement(
+ "Screen",
+ null,
+ title ? React.createElement("Text", null, title) : null,
+ subtitle ? React.createElement("Text", null, subtitle) : null,
+ loading ? React.createElement("Text", null, "Loading") : children,
+ ),
+ };
+});
+
+vi.mock("@/components/GlassSurface", async () => {
+ const React = await import("react");
+ return {
+ GlassSurface: ({ children }: { children?: ReactNode }) =>
+ React.createElement("GlassSurface", null, children),
+ };
+});
+
+vi.mock("@/components/CapCard", async () => {
+ const React = await import("react");
+ return {
+ CapCard: (props: HostProps) => React.createElement("CapCard", props),
+ };
+});
+
+vi.mock("@/components/OrgSwitcher", async () => {
+ const React = await import("react");
+ return {
+ OrgSwitcher: () => React.createElement("OrgSwitcher"),
+ };
+});
+
+vi.mock("expo-symbols", () => ({
+ SymbolView: () => null,
+}));
+
+vi.mock("react-native-svg", async () => {
+ const React = await import("react");
+ const createHost =
+ (name: string) =>
+ ({ children, ...props }: HostProps) =>
+ React.createElement(name, props, children);
+
+ return {
+ default: createHost("Svg"),
+ Path: createHost("Path"),
+ Rect: createHost("Rect"),
+ };
+});
+
+vi.mock("@/theme", () => ({
+ colors: {
+ appBackground: "#f9f9f9",
+ black: "#000000",
+ blackAlpha40: "rgba(18, 22, 31, 0.4)",
+ blue11: "#0d74ce",
+ blue3: "#edf6ff",
+ blue6: "#acd8fc",
+ blue9: "#0090ff",
+ buttonBlue: "#2563eb",
+ buttonBlueBorder: "#1e40af",
+ glass: "rgba(252, 252, 252, 0.72)",
+ gray1: "#fcfcfc",
+ gray10: "#838383",
+ gray12: "#202020",
+ gray2: "#f9f9f9",
+ gray3: "#f0f0f0",
+ gray4: "#e8e8e8",
+ gray5: "#e0e0e0",
+ gray6: "#d9d9d9",
+ gray9: "#8d8d8d",
+ red1: "#fffcfc",
+ red3: "#feebec",
+ red6: "#fdbdbe",
+ red9: "#e5484d",
+ white: "#ffffff",
+ yellow3: "#fffab8",
+ yellow5: "#ffe770",
+ yellow9: "#f5d90a",
+ },
+ fonts: {
+ bold: "NeueMontreal-Bold",
+ medium: "NeueMontreal-Medium",
+ regular: "NeueMontreal-Regular",
+ },
+ radius: {
+ full: 999,
+ lg: 16,
+ md: 12,
+ sm: 8,
+ xl: 20,
+ xs: 6,
+ },
+ shadows: {
+ card: {},
+ popover: {},
+ },
+ squircle: {
+ borderCurve: "continuous",
+ },
+}));
+
+vi.mock("expo-document-picker", () => ({
+ getDocumentAsync: vi.fn(),
+}));
+
+vi.mock("expo-file-system/legacy", () => ({
+ documentDirectory: "file:///tmp/",
+ downloadAsync: vi.fn(),
+}));
+
+vi.mock("expo-image-picker", () => ({
+ launchImageLibraryAsync: vi.fn(),
+ requestMediaLibraryPermissionsAsync: vi.fn(),
+}));
+
+vi.mock("expo-media-library", () => ({
+ requestPermissionsAsync: vi.fn(),
+ saveToLibraryAsync: vi.fn(),
+}));
+
+vi.mock("@/uploads/runMobileUpload", () => ({
+ runMobileUpload: vi.fn(),
+}));
+
+vi.mock("@/uploads/uploadQueue", async (importOriginal) => {
+ const actual = await importOriginal();
+
+ return {
+ ...actual,
+ emptyUploadQueue: uploadQueueState.value,
+ uploadProgressPercent: (progress: number) => Math.round(progress * 100),
+ uploadQueueReducer: (state: { items: unknown[] }, action: unknown) => {
+ uploadQueueActionsState.value.push(action);
+ return state;
+ },
+ uploadQueueStatusText: (item: {
+ processingMessage?: string | null;
+ progress: number;
+ status: string;
+ }) => {
+ if (item.status === "complete") return "Ready to view";
+ if (item.status === "failed") return "Upload failed";
+ if (item.status === "processing") {
+ return item.processingMessage ?? "Finishing up";
+ }
+ if (item.status === "uploading") {
+ return `Uploading ${Math.round(item.progress * 100)}%`;
+ }
+ return "Queued";
+ },
+ };
+});
+
+describe("upload and dashboard visibility", () => {
+ beforeEach(() => {
+ authState.value = createAuth();
+ uploadQueueState.value.items = [];
+ uploadQueueActionsState.value = [];
+ });
+
+ it("shows native upload entry points", async () => {
+ const tree = await renderTree(React.createElement(UploadScreen));
+
+ expect(getTextNodes(tree)).toContain("Import");
+ expect(getTextNodes(tree)).toContain("Upload File");
+ expect(getTextNodes(tree)).toContain("Browse Files");
+ expect(getTextNodes(tree)).toContain("Photos");
+ expect(getTextNodes(tree)).toContain("Import from Loom");
+ expect(getTextNodes(tree)).toContain("MP4, MOV, AVI, MKV, WebM, or M4V");
+ expect(hasStyle(tree, { height: 128, backgroundColor: "#f0f0f0" })).toBe(
+ true,
+ );
+ expect(
+ hasProps(tree, {
+ accessibilityHint: "Opens upload source options",
+ accessibilityLabel: "Choose upload source",
+ accessibilityState: { busy: false, disabled: false },
+ accessibilityValue: {
+ text: "MP4, MOV, AVI, MKV, WebM, or M4V",
+ },
+ }),
+ ).toBe(true);
+ expect(
+ hasProps(tree, {
+ accessibilityHint: "Opens Loom import in a browser sheet",
+ accessibilityLabel: "Open Loom import",
+ }),
+ ).toBe(true);
+ expect(
+ hasProps(tree, {
+ accessibilityHint: "Opens the native file picker",
+ accessibilityLabel: "Browse Files",
+ }),
+ ).toBe(true);
+ expect(
+ hasProps(tree, {
+ accessibilityHint: "Opens your photo library",
+ accessibilityLabel: "Photos",
+ }),
+ ).toBe(true);
+ });
+
+ it("opens the native iOS upload source sheet", async () => {
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [uploadSource] = renderer.root.findAllByProps({
+ accessibilityLabel: "Choose upload source",
+ });
+ if (!uploadSource) throw new Error("Upload source button was not rendered");
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ uploadSource.props.onPress();
+ });
+
+ expect(showActionSheetWithOptions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cancelButtonIndex: 3,
+ options: ["Browse Files", "Photos", "Import from Loom", "Cancel"],
+ tintColor: "#0d74ce",
+ title: "Upload File",
+ userInterfaceStyle: "light",
+ }),
+ expect.any(Function),
+ );
+
+ const [, callback] = showActionSheetWithOptions.mock.calls[0] ?? [];
+ if (!callback) throw new Error("Upload source callback was not set");
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ openBrowserAsync.mockClear();
+
+ await act(async () => {
+ callback(2);
+ });
+
+ expect(openBrowserAsync).toHaveBeenCalledWith(
+ "https://cap.so/dashboard/import/loom",
+ );
+ });
+
+ it("opens Loom import in the native browser sheet", async () => {
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [loomAction] = renderer.root.findAllByProps({
+ accessibilityLabel: "Import from Loom",
+ });
+ const [loomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Open Loom import",
+ });
+ if (!loomAction) throw new Error("Loom upload action was not rendered");
+ if (!loomImport) throw new Error("Loom import card was not rendered");
+
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ openBrowserAsync.mockClear();
+
+ expect(loomAction.props.accessibilityHint).toBe(
+ "Opens Loom import in a browser sheet",
+ );
+
+ await act(async () => {
+ loomAction.props.onPress();
+ });
+
+ expect(openBrowserAsync).toHaveBeenCalledWith(
+ "https://cap.so/dashboard/import/loom",
+ );
+ openBrowserAsync.mockClear();
+
+ await act(async () => {
+ loomImport.props.onPress();
+ });
+
+ expect(openBrowserAsync).toHaveBeenCalledWith(
+ "https://cap.so/dashboard/import/loom",
+ );
+ });
+
+ it("shows Loom import failures on the Loom card", async () => {
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ openBrowserAsync.mockClear();
+ openBrowserAsync.mockRejectedValueOnce(new Error("Loom unavailable"));
+
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [loomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Open Loom import",
+ });
+ if (!loomImport) throw new Error("Loom import card was not rendered");
+
+ await act(async () => {
+ loomImport.props.onPress();
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "Loom import unavailable",
+ );
+ expect(getTextNodes(renderer.toJSON())).toContain("Loom unavailable");
+ const [failedLoomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Loom import unavailable",
+ });
+ if (!failedLoomImport) throw new Error("Loom error card was not rendered");
+ expect(failedLoomImport.props.accessibilityHint).toBe(
+ "Retries Loom import",
+ );
+ expect(failedLoomImport.props.accessibilityValue).toEqual({
+ text: "Loom unavailable",
+ });
+ expect(failedLoomImport.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: false,
+ });
+ const [retryLoomAction] = renderer.root.findAllByProps({
+ accessibilityLabel: "Retry Loom",
+ });
+ if (!retryLoomAction) throw new Error("Retry Loom action was not rendered");
+ expect(retryLoomAction.props.accessibilityHint).toBe("Loom unavailable");
+ expect(retryLoomAction.props.accessibilityValue).toEqual({
+ text: "Loom unavailable",
+ });
+ expect(retryLoomAction.props.disabled).toBe(false);
+ const [uploadSource] = renderer.root.findAllByProps({
+ accessibilityLabel: "Choose upload source",
+ });
+ if (!uploadSource) throw new Error("Upload source card was not rendered");
+ expect(uploadSource.props.accessibilityValue).toEqual({
+ text: "MP4, MOV, AVI, MKV, WebM, or M4V",
+ });
+ expect(hasStyle(renderer.toJSON(), { color: "#e5484d" })).toBe(true);
+ expect(
+ hasProps(renderer.toJSON(), {
+ accessibilityLiveRegion: "polite",
+ accessibilityRole: "alert",
+ }),
+ ).toBe(true);
+ });
+
+ it("locks stale Loom import actions while the browser sheet is opening", async () => {
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ let resolveBrowser:
+ | ((
+ value: Awaited>,
+ ) => void)
+ | null = null;
+ openBrowserAsync.mockClear();
+ openBrowserAsync.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveBrowser = resolve;
+ }),
+ );
+
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [loomAction] = renderer.root.findAllByProps({
+ accessibilityLabel: "Import from Loom",
+ });
+ const [loomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Open Loom import",
+ });
+ if (!loomAction) throw new Error("Loom upload action was not rendered");
+ if (!loomImport) throw new Error("Loom import card was not rendered");
+
+ await act(async () => {
+ void loomAction.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [uploadSource] = renderer.root.findAllByProps({
+ accessibilityLabel: "Choose upload source",
+ });
+ const [loadingLoomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Opening Loom",
+ });
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ const [loadingLoomAction] = renderer.root.findAllByProps({
+ accessibilityLabel: "Import from Loom",
+ });
+ if (!uploadSource) throw new Error("Upload source button was not rendered");
+ if (!browseButton) throw new Error("Browse Files button was not rendered");
+ if (!loadingLoomAction)
+ throw new Error("Loom upload action was not rendered");
+ if (!loadingLoomImport)
+ throw new Error("Loom import card was not rendered");
+
+ const loadingText = getTextNodes(renderer.toJSON());
+ expect(loadingText.filter((item) => item === "Opening Loom")).toHaveLength(
+ 1,
+ );
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Opening Loom...");
+ expect(
+ loadingText.filter(
+ (item) => item === "Continue in the browser sheet to import from Loom.",
+ ),
+ ).toHaveLength(1);
+ expect(uploadSource.props.accessibilityHint).toBe("Loom import is opening");
+ expect(uploadSource.props.accessibilityValue).toEqual({
+ text: "Opening Loom import",
+ });
+ expect(uploadSource.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(browseButton.props.disabled).toBe(true);
+ expect(browseButton.props.accessibilityHint).toBe("Loom import is opening");
+ expect(browseButton.props.accessibilityValue).toEqual({
+ text: "Opening Loom import",
+ });
+ expect(loadingLoomAction.props.accessibilityHint).toBe(
+ "Loom import is opening",
+ );
+ expect(loadingLoomAction.props.accessibilityValue).toEqual({
+ text: "Opening Loom import",
+ });
+ expect(loadingLoomAction.props.loading).toBe(true);
+ expect(loadingLoomAction.props.disabled).toBe(false);
+ expect(loadingLoomImport.props.accessibilityHint).toBe(
+ "Loom import is opening",
+ );
+ expect(loadingLoomImport.props.accessibilityValue).toEqual({
+ text: "Opening Loom import",
+ });
+ expect(loadingLoomImport.props.disabled).toBe(true);
+ expect(openBrowserAsync).toHaveBeenCalledTimes(1);
+
+ openBrowserAsync.mockClear();
+
+ await act(async () => {
+ loomAction.props.onPress();
+ loomImport.props.onPress();
+ uploadSource.props.onPress();
+ await Promise.resolve();
+ });
+
+ expect(openBrowserAsync).not.toHaveBeenCalled();
+
+ await act(async () => {
+ resolveBrowser?.({
+ type: "dismiss",
+ } as Awaited>);
+ await Promise.resolve();
+ });
+ });
+
+ it("locks upload sources while the file picker is opening", async () => {
+ const DocumentPicker = await import("expo-document-picker");
+ let resolvePicker:
+ | ((
+ value: Awaited>,
+ ) => void)
+ | null = null;
+ vi.mocked(DocumentPicker.getDocumentAsync).mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolvePicker = resolve;
+ }),
+ );
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ if (!browseButton) throw new Error("Browse Files button was not rendered");
+
+ await act(async () => {
+ void browseButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [uploadSource] = renderer.root.findAllByProps({
+ accessibilityLabel: "Opening Files",
+ });
+ const [loadingBrowseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ const [photosButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Photos",
+ });
+ const [loomAction] = renderer.root.findAllByProps({
+ accessibilityLabel: "Import from Loom",
+ });
+ const [loomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Open Loom import",
+ });
+ if (
+ !uploadSource ||
+ !loadingBrowseButton ||
+ !photosButton ||
+ !loomAction ||
+ !loomImport
+ ) {
+ throw new Error("Upload source controls were not rendered");
+ }
+
+ expect(getTextNodes(renderer.toJSON())).toContain("Opening Files");
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Opening Files...");
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "Choose a video from Files.",
+ );
+ expect(uploadSource.props.accessibilityHint).toBe(
+ "Upload source picker is opening",
+ );
+ expect(uploadSource.props.accessibilityValue).toEqual({
+ text: "Opening native file picker",
+ });
+ expect(uploadSource.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(uploadSource.props.disabled).toBe(true);
+ expect(resolveStyle(uploadSource.props.style)).toMatchObject({
+ opacity: 0.58,
+ });
+ expect(loadingBrowseButton.props.loading).toBe(true);
+ expect(loadingBrowseButton.props.accessibilityHint).toBe(
+ "Upload source picker is opening",
+ );
+ expect(loadingBrowseButton.props.accessibilityValue).toEqual({
+ text: "Opening native file picker",
+ });
+ expect(photosButton.props.accessibilityHint).toBe(
+ "Another upload source is opening",
+ );
+ expect(photosButton.props.accessibilityValue).toEqual({
+ text: "Opening native file picker",
+ });
+ expect(photosButton.props.disabled).toBe(true);
+ expect(loomAction.props.accessibilityHint).toBe(
+ "Another upload source is opening",
+ );
+ expect(loomAction.props.accessibilityValue).toEqual({
+ text: "Opening native file picker",
+ });
+ expect(loomAction.props.disabled).toBe(true);
+ expect(loomImport.props.accessibilityHint).toBe(
+ "Upload source picker is opening",
+ );
+ expect(loomImport.props.accessibilityValue).toEqual({
+ text: "Opening native file picker",
+ });
+ expect(loomImport.props.disabled).toBe(true);
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ const ImagePicker = await import("expo-image-picker");
+ const requestMediaLibraryPermissionsAsync = vi.mocked(
+ ImagePicker.requestMediaLibraryPermissionsAsync,
+ );
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ showActionSheetWithOptions.mockClear();
+ requestMediaLibraryPermissionsAsync.mockClear();
+ openBrowserAsync.mockClear();
+
+ await act(async () => {
+ uploadSource.props.onPress();
+ photosButton.props.onPress();
+ loomAction.props.onPress();
+ loomImport.props.onPress();
+ });
+
+ expect(showActionSheetWithOptions).not.toHaveBeenCalled();
+ expect(requestMediaLibraryPermissionsAsync).not.toHaveBeenCalled();
+ expect(openBrowserAsync).not.toHaveBeenCalled();
+ expect(DocumentPicker.getDocumentAsync).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ resolvePicker?.({
+ assets: null,
+ canceled: true,
+ } as Awaited>);
+ await Promise.resolve();
+ });
+ });
+
+ it("shows the active Photos source as loading while the photo picker is opening", async () => {
+ const ImagePicker = await import("expo-image-picker");
+ const requestMediaLibraryPermissionsAsync = vi.mocked(
+ ImagePicker.requestMediaLibraryPermissionsAsync,
+ );
+ let resolvePermission:
+ | ((
+ value: Awaited<
+ ReturnType
+ >,
+ ) => void)
+ | null = null;
+ requestMediaLibraryPermissionsAsync.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolvePermission = resolve;
+ }),
+ );
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [photosButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Photos",
+ });
+ if (!photosButton) throw new Error("Photos button was not rendered");
+
+ await act(async () => {
+ void photosButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const [uploadSource] = renderer.root.findAllByProps({
+ accessibilityLabel: "Opening Photos",
+ });
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ const [loadingPhotosButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Photos",
+ });
+ const [loomAction] = renderer.root.findAllByProps({
+ accessibilityLabel: "Import from Loom",
+ });
+ const [loomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Open Loom import",
+ });
+ if (
+ !uploadSource ||
+ !browseButton ||
+ !loadingPhotosButton ||
+ !loomAction ||
+ !loomImport
+ ) {
+ throw new Error("Upload source controls were not rendered");
+ }
+
+ expect(getTextNodes(renderer.toJSON())).toContain("Opening Photos");
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Opening Photos...");
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "Choose a video from Photos.",
+ );
+ expect(uploadSource.props.accessibilityHint).toBe(
+ "Upload source picker is opening",
+ );
+ expect(uploadSource.props.accessibilityValue).toEqual({
+ text: "Opening native photo picker",
+ });
+ expect(uploadSource.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(uploadSource.props.disabled).toBe(true);
+ expect(resolveStyle(uploadSource.props.style)).toMatchObject({
+ opacity: 0.58,
+ });
+ expect(browseButton.props.accessibilityHint).toBe(
+ "Another upload source is opening",
+ );
+ expect(browseButton.props.accessibilityValue).toEqual({
+ text: "Opening native photo picker",
+ });
+ expect(browseButton.props.disabled).toBe(true);
+ expect(loadingPhotosButton.props.accessibilityHint).toBe(
+ "Upload source picker is opening",
+ );
+ expect(loadingPhotosButton.props.accessibilityValue).toEqual({
+ text: "Opening native photo picker",
+ });
+ expect(loadingPhotosButton.props.loading).toBe(true);
+ expect(loadingPhotosButton.props.disabled).toBe(false);
+ expect(loomAction.props.accessibilityHint).toBe(
+ "Another upload source is opening",
+ );
+ expect(loomAction.props.accessibilityValue).toEqual({
+ text: "Opening native photo picker",
+ });
+ expect(loomAction.props.disabled).toBe(true);
+ expect(loomImport.props.accessibilityHint).toBe(
+ "Upload source picker is opening",
+ );
+ expect(loomImport.props.accessibilityValue).toEqual({
+ text: "Opening native photo picker",
+ });
+ expect(loomImport.props.disabled).toBe(true);
+
+ await act(async () => {
+ resolvePermission?.({
+ granted: false,
+ } as Awaited<
+ ReturnType
+ >);
+ await Promise.resolve();
+ });
+
+ expect(showActionSheetWithOptions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cancelButtonIndex: 1,
+ message: "Allow Cap to read videos from Photos before uploading.",
+ options: ["Open Settings", "Cancel"],
+ title: "Photos access needed",
+ }),
+ expect.any(Function),
+ );
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "Upload source unavailable",
+ );
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "Allow Cap to read videos from Photos before uploading.",
+ );
+ const [failedUploadSource] = renderer.root.findAllByProps({
+ accessibilityLabel: "Upload source unavailable",
+ });
+ const [retryPhotosButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Retry Photos",
+ });
+ if (!failedUploadSource)
+ throw new Error("Upload source error state was not rendered");
+ if (!retryPhotosButton)
+ throw new Error("Retry Photos button was not rendered");
+ expect(failedUploadSource.props.accessibilityValue).toEqual({
+ text: "Allow Cap to read videos from Photos before uploading.",
+ });
+ expect(retryPhotosButton.props.accessibilityHint).toBe(
+ "Allow Cap to read videos from Photos before uploading.",
+ );
+ expect(retryPhotosButton.props.accessibilityValue).toEqual({
+ text: "Allow Cap to read videos from Photos before uploading.",
+ });
+ expect(retryPhotosButton.props.disabled).toBe(false);
+ });
+
+ it("deduplicates stale upload source actions while the file picker is opening", async () => {
+ const DocumentPicker = await import("expo-document-picker");
+ const getDocumentAsync = vi.mocked(DocumentPicker.getDocumentAsync);
+ let resolvePicker:
+ | ((
+ value: Awaited>,
+ ) => void)
+ | null = null;
+ getDocumentAsync.mockClear();
+ getDocumentAsync.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolvePicker = resolve;
+ }),
+ );
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [uploadSource] = renderer.root.findAllByProps({
+ accessibilityLabel: "Choose upload source",
+ });
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ const [photosButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Photos",
+ });
+ const [loomAction] = renderer.root.findAllByProps({
+ accessibilityLabel: "Import from Loom",
+ });
+ const [loomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Open Loom import",
+ });
+ if (
+ !uploadSource ||
+ !browseButton ||
+ !photosButton ||
+ !loomAction ||
+ !loomImport
+ ) {
+ throw new Error("Upload source controls were not rendered");
+ }
+
+ await act(async () => {
+ void browseButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ const ImagePicker = await import("expo-image-picker");
+ const requestMediaLibraryPermissionsAsync = vi.mocked(
+ ImagePicker.requestMediaLibraryPermissionsAsync,
+ );
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ showActionSheetWithOptions.mockClear();
+ requestMediaLibraryPermissionsAsync.mockClear();
+ openBrowserAsync.mockClear();
+
+ await act(async () => {
+ uploadSource.props.onPress();
+ browseButton.props.onPress();
+ photosButton.props.onPress();
+ loomAction.props.onPress();
+ loomImport.props.onPress();
+ await Promise.resolve();
+ });
+
+ expect(getDocumentAsync).toHaveBeenCalledTimes(1);
+ expect(showActionSheetWithOptions).not.toHaveBeenCalled();
+ expect(requestMediaLibraryPermissionsAsync).not.toHaveBeenCalled();
+ expect(openBrowserAsync).not.toHaveBeenCalled();
+
+ await act(async () => {
+ resolvePicker?.({
+ assets: null,
+ canceled: true,
+ } as Awaited>);
+ await Promise.resolve();
+ });
+ });
+
+ it("locks Loom import while a device upload is active", async () => {
+ const DocumentPicker = await import("expo-document-picker");
+ const { runMobileUpload } = await import("@/uploads/runMobileUpload");
+ const uploadStartedAt = 1_763_440_800_000;
+ const dateNow = vi.spyOn(Date, "now").mockReturnValue(uploadStartedAt);
+ let resolveUpload:
+ | ((value: Awaited>) => void)
+ | null = null;
+ uploadQueueState.value.items = [
+ {
+ capId: null,
+ contentType: "video/mp4",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ error: null,
+ fileName: "launch-review.mp4",
+ folderId: null,
+ id: `${uploadStartedAt}-launch-review.mp4`,
+ localUri: "file:///tmp/launch-review.mp4",
+ organizationId: "org_123",
+ progress: 0,
+ processingMessage: null,
+ rawFileKey: null,
+ size: 12_400_000,
+ status: "queued",
+ updatedAt: "2026-05-18T10:00:00.000Z",
+ },
+ ];
+ vi.mocked(DocumentPicker.getDocumentAsync).mockResolvedValueOnce({
+ assets: [
+ {
+ mimeType: "video/mp4",
+ name: "launch-review.mp4",
+ size: 12_400_000,
+ uri: "file:///tmp/launch-review.mp4",
+ },
+ ],
+ canceled: false,
+ } as Awaited>);
+ vi.mocked(runMobileUpload).mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveUpload = resolve;
+ }),
+ );
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ if (!browseButton) throw new Error("Browse Files button was not rendered");
+
+ await act(async () => {
+ void browseButton.props.onPress();
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ const [uploadSource] = renderer.root.findAllByProps({
+ accessibilityLabel: "Choose upload source",
+ });
+ const [loomImport] = renderer.root.findAllByProps({
+ accessibilityLabel: "Open Loom import",
+ });
+ const [activeBrowseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ const [photosButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Photos",
+ });
+ const [loomAction] = renderer.root.findAllByProps({
+ accessibilityLabel: "Import from Loom",
+ });
+ if (!uploadSource) throw new Error("Upload source button was not rendered");
+ if (!loomImport) throw new Error("Loom import card was not rendered");
+ if (!activeBrowseButton)
+ throw new Error("Browse Files button was not rendered");
+ if (!photosButton) throw new Error("Photos button was not rendered");
+ if (!loomAction) throw new Error("Loom upload action was not rendered");
+ const WebBrowser = await import("expo-web-browser");
+ const openBrowserAsync = vi.mocked(WebBrowser.openBrowserAsync);
+ openBrowserAsync.mockClear();
+
+ expect(getTextNodes(renderer.toJSON())).toContain("Upload File");
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Preparing upload");
+ expect(getTextNodes(renderer.toJSON()).join("")).toContain(
+ "Preparing upload ยท 12 MB",
+ );
+ expect(getTextNodes(renderer.toJSON())).toContain("Import from Loom");
+ expect(getTextNodes(renderer.toJSON())).toContain(
+ "Finish preparing this upload before importing from Loom.",
+ );
+ expect(uploadSource.props.accessibilityHint).toBe("Preparing upload");
+ expect(uploadSource.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(uploadSource.props.disabled).toBe(true);
+ expect(resolveStyle(uploadSource.props.style)).toMatchObject({
+ opacity: 0.58,
+ });
+ expect(activeBrowseButton.props.loading).toBe(false);
+ expect(activeBrowseButton.props.accessibilityHint).toBe("Preparing upload");
+ expect(activeBrowseButton.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(activeBrowseButton.props.disabled).toBe(true);
+ expect(photosButton.props.loading).toBe(false);
+ expect(photosButton.props.accessibilityHint).toBe("Preparing upload");
+ expect(photosButton.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(photosButton.props.disabled).toBe(true);
+ expect(loomAction.props.loading).toBe(false);
+ expect(loomAction.props.accessibilityHint).toBe("Preparing upload");
+ expect(loomAction.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(loomAction.props.disabled).toBe(true);
+ expect(loomImport.props.accessibilityHint).toBe("Preparing upload");
+ expect(loomImport.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(loomImport.props.accessibilityState).toEqual({
+ busy: true,
+ disabled: true,
+ });
+ expect(loomImport.props.disabled).toBe(true);
+
+ await act(async () => {
+ loomImport.props.onPress();
+ });
+
+ expect(openBrowserAsync).not.toHaveBeenCalled();
+
+ await act(async () => {
+ resolveUpload?.({
+ id: "video_123",
+ } as Awaited>);
+ await Promise.resolve();
+ });
+ dateNow.mockRestore();
+ });
+
+ it("locks inactive upload queue rows while a device upload is active", async () => {
+ uploadQueueState.value.items = [
+ {
+ capId: null,
+ contentType: "video/mp4",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ error: "Network unavailable",
+ fileName: "failed-upload.mp4",
+ folderId: null,
+ id: "failed-upload",
+ localUri: "file:///tmp/failed-upload.mp4",
+ organizationId: "org_123",
+ progress: 0.42,
+ rawFileKey: null,
+ size: 124_000,
+ durationSeconds: 125,
+ status: "failed",
+ updatedAt: "2026-05-18T10:00:00.000Z",
+ },
+ ];
+ const DocumentPicker = await import("expo-document-picker");
+ const { runMobileUpload } = await import("@/uploads/runMobileUpload");
+ let resolveUpload:
+ | ((value: Awaited>) => void)
+ | null = null;
+ vi.mocked(DocumentPicker.getDocumentAsync).mockResolvedValueOnce({
+ assets: [
+ {
+ mimeType: "video/mp4",
+ name: "launch-review.mp4",
+ size: 12_400_000,
+ uri: "file:///tmp/launch-review.mp4",
+ },
+ ],
+ canceled: false,
+ } as Awaited>);
+ vi.mocked(runMobileUpload).mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveUpload = resolve;
+ }),
+ );
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ if (!browseButton) throw new Error("Browse Files button was not rendered");
+
+ await act(async () => {
+ void browseButton.props.onPress();
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ const [queueRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "Upload failed-upload.mp4",
+ });
+ const [retryButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Retry upload failed-upload.mp4",
+ });
+ const queueMenus = renderer.root.findAllByProps({
+ accessibilityLabel: "More actions for failed-upload.mp4",
+ });
+ const [queueMenu] = queueMenus;
+ if (!queueRow) throw new Error("Upload queue row was not rendered");
+ if (!retryButton) throw new Error("Retry button was not rendered");
+ if (!queueMenu) throw new Error("Upload queue menu was not rendered");
+
+ expect(queueRow.props.accessibilityHint).toBe(
+ "Another upload is in progress",
+ );
+ expect(queueRow.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: true,
+ });
+ expect(queueRow.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(queueRow.props.disabled).toBe(true);
+ expect(retryButton.props.disabled).toBe(true);
+ expect(retryButton.props.accessibilityHint).toBe(
+ "Another upload is in progress",
+ );
+ expect(retryButton.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(queueMenu.props.accessibilityHint).toBe(
+ "Another upload is in progress",
+ );
+ expect(queueMenu.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: true,
+ });
+ expect(queueMenu.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(queueMenu.props.disabled).toBe(true);
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ queueRow.props.onPress();
+ retryButton.props.onPress({ stopPropagation: vi.fn() });
+ queueMenu.props.onPress({ stopPropagation: vi.fn() });
+ });
+
+ expect(showActionSheetWithOptions).not.toHaveBeenCalled();
+ expect(uploadQueueActionsState.value).not.toContainEqual(
+ expect.objectContaining({
+ id: "failed-upload",
+ type: "retry",
+ }),
+ );
+
+ await act(async () => {
+ resolveUpload?.({
+ id: "video_123",
+ } as Awaited>);
+ await Promise.resolve();
+ });
+ });
+
+ it("locks stale upload queue view actions while a device upload is active", async () => {
+ uploadQueueState.value.items = [
+ {
+ capId: "video_complete",
+ contentType: "video/mp4",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ error: null,
+ fileName: "processed-upload.mp4",
+ folderId: null,
+ id: "processed-upload",
+ localUri: "file:///tmp/processed-upload.mp4",
+ organizationId: "org_123",
+ progress: 1,
+ rawFileKey: "raw-file-key",
+ size: 124_000,
+ durationSeconds: 125,
+ status: "complete",
+ updatedAt: "2026-05-18T10:00:00.000Z",
+ },
+ ];
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [queueRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "Upload processed-upload.mp4",
+ });
+ const [queueMenu] = renderer.root.findAllByProps({
+ accessibilityLabel: "More actions for processed-upload.mp4",
+ });
+ const [viewButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "View upload processed-upload.mp4",
+ });
+ if (!queueRow) throw new Error("Upload queue row was not rendered");
+ if (!queueMenu) throw new Error("Upload queue menu was not rendered");
+ if (!viewButton) throw new Error("View button was not rendered");
+ expect(queueRow.props.accessibilityHint).toBe(
+ "Ready to view. Opens upload actions",
+ );
+ expect(queueMenu.props.accessibilityHint).toBe(
+ "Opens view and remove actions",
+ );
+ expect(queueRow.props.accessibilityValue).toEqual({
+ text: "Ready to view ยท 124 KB ยท 2 mins",
+ });
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ queueRow.props.onPress();
+ });
+
+ const [, callback] = showActionSheetWithOptions.mock.calls[0] ?? [];
+ if (!callback) throw new Error("Upload queue action callback was not set");
+
+ const DocumentPicker = await import("expo-document-picker");
+ const { runMobileUpload } = await import("@/uploads/runMobileUpload");
+ let resolveUpload:
+ | ((value: Awaited>) => void)
+ | null = null;
+ vi.mocked(DocumentPicker.getDocumentAsync).mockResolvedValueOnce({
+ assets: [
+ {
+ mimeType: "video/mp4",
+ name: "launch-review.mp4",
+ size: 12_400_000,
+ uri: "file:///tmp/launch-review.mp4",
+ },
+ ],
+ canceled: false,
+ } as Awaited>);
+ vi.mocked(runMobileUpload).mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveUpload = resolve;
+ }),
+ );
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ if (!browseButton) throw new Error("Browse Files button was not rendered");
+
+ await act(async () => {
+ void browseButton.props.onPress();
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ const { router } = await import("expo-router");
+ const push = vi.mocked(router.push);
+ push.mockClear();
+ const [lockedViewButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "View upload processed-upload.mp4",
+ });
+ const [lockedQueueMenu] = renderer.root.findAllByProps({
+ accessibilityLabel: "More actions for processed-upload.mp4",
+ });
+ if (!lockedViewButton) throw new Error("View button was not rendered");
+ if (!lockedQueueMenu) throw new Error("Upload queue menu was not rendered");
+ expect(lockedViewButton.props.accessibilityHint).toBe(
+ "Another upload is in progress",
+ );
+ expect(lockedViewButton.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(lockedViewButton.props.disabled).toBe(true);
+ expect(lockedQueueMenu.props.accessibilityHint).toBe(
+ "Another upload is in progress",
+ );
+ expect(lockedQueueMenu.props.accessibilityState).toEqual({
+ busy: false,
+ disabled: true,
+ });
+ expect(lockedQueueMenu.props.accessibilityValue).toEqual({
+ text: "Preparing upload launch-review.mp4",
+ });
+ expect(lockedQueueMenu.props.disabled).toBe(true);
+
+ showActionSheetWithOptions.mockClear();
+ await act(async () => {
+ viewButton.props.onPress({ stopPropagation: vi.fn() });
+ lockedQueueMenu.props.onPress({ stopPropagation: vi.fn() });
+ callback(0);
+ });
+
+ expect(push).not.toHaveBeenCalled();
+ expect(showActionSheetWithOptions).not.toHaveBeenCalled();
+
+ await act(async () => {
+ resolveUpload?.({
+ id: "video_123",
+ } as Awaited>);
+ await Promise.resolve();
+ });
+ });
+
+ it("announces processing upload queue rows with their current status", async () => {
+ uploadQueueState.value.items = [
+ {
+ capId: "video_processing",
+ contentType: "video/mp4",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ error: null,
+ fileName: "processing-upload.mp4",
+ folderId: null,
+ id: "processing-upload",
+ localUri: "file:///tmp/processing-upload.mp4",
+ organizationId: "org_123",
+ progress: 0.42,
+ processingMessage: "Processing frames",
+ rawFileKey: "raw-file-key",
+ size: 124_000,
+ durationSeconds: 125,
+ status: "processing",
+ updatedAt: "2026-05-18T10:00:00.000Z",
+ },
+ ];
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const tree = renderer.toJSON();
+ const [queueRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "Upload processing-upload.mp4",
+ });
+ const [queueMenu] = renderer.root.findAllByProps({
+ accessibilityLabel: "More actions for processing-upload.mp4",
+ });
+ const [viewButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "View upload processing-upload.mp4",
+ });
+ if (!queueRow) throw new Error("Processing upload row was not rendered");
+ if (!queueMenu) throw new Error("Upload queue menu was not rendered");
+ if (!viewButton) throw new Error("View button was not rendered");
+
+ expect(getTextNodes(tree).join("")).toContain(
+ "Processing frames ยท 124 KB ยท 2 mins",
+ );
+ expect(queueRow.props.accessibilityHint).toBe(
+ "Processing frames. Opens upload actions",
+ );
+ expect(queueMenu.props.accessibilityHint).toBe(
+ "Opens view and remove actions",
+ );
+ expect(queueRow.props.accessibilityValue).toEqual({
+ text: "Processing frames ยท 124 KB ยท 2 mins",
+ });
+ expect(
+ hasProps(tree, {
+ accessibilityLabel: "Upload progress for processing-upload.mp4",
+ accessibilityRole: "progressbar",
+ accessibilityValue: {
+ max: 100,
+ min: 0,
+ now: 42,
+ text: "42%",
+ },
+ }),
+ ).toBe(true);
+ expect(viewButton.props.accessibilityHint).toBe("Opens the uploaded Cap");
+ });
+
+ it("shows queued upload rows without premature progress", async () => {
+ uploadQueueState.value.items = [
+ {
+ capId: null,
+ contentType: "video/mp4",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ error: null,
+ fileName: "queued-upload.mp4",
+ folderId: null,
+ id: "queued-upload",
+ localUri: "file:///tmp/queued-upload.mp4",
+ organizationId: "org_123",
+ progress: 0,
+ processingMessage: null,
+ rawFileKey: null,
+ size: 124_000,
+ durationSeconds: 125,
+ status: "queued",
+ updatedAt: "2026-05-18T10:00:00.000Z",
+ },
+ ];
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const tree = renderer.toJSON();
+ const [queueRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "Upload queued-upload.mp4",
+ });
+ const [queueMenu] = renderer.root.findAllByProps({
+ accessibilityLabel: "More actions for queued-upload.mp4",
+ });
+ if (!queueRow) throw new Error("Queued upload row was not rendered");
+ if (!queueMenu) throw new Error("Upload queue menu was not rendered");
+
+ expect(getTextNodes(tree).join("")).toContain("Queued ยท 124 KB ยท 2 mins");
+ expect(queueRow.props.accessibilityHint).toBe(
+ "Queued. Opens upload actions",
+ );
+ expect(queueRow.props.accessibilityValue).toEqual({
+ text: "Queued ยท 124 KB ยท 2 mins",
+ });
+ expect(queueMenu.props.accessibilityHint).toBe("Opens remove action");
+ expect(
+ hasProps(tree, {
+ accessibilityLabel: "Upload progress for queued-upload.mp4",
+ accessibilityRole: "progressbar",
+ }),
+ ).toBe(false);
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ queueMenu.props.onPress({ stopPropagation: vi.fn() });
+ });
+
+ expect(showActionSheetWithOptions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cancelButtonIndex: 1,
+ destructiveButtonIndex: 0,
+ message: "Queued ยท 124 KB ยท 2 mins",
+ options: ["Remove from Queue", "Cancel"],
+ title: "queued-upload.mp4",
+ userInterfaceStyle: "light",
+ }),
+ expect.any(Function),
+ );
+ });
+
+ it("keeps uploaded files processing when the library refresh fails", async () => {
+ const auth = createAuth();
+ auth.refresh = vi.fn(() => Promise.reject(new Error("Refresh failed")));
+ authState.value = auth;
+ const DocumentPicker = await import("expo-document-picker");
+ const { runMobileUpload } = await import("@/uploads/runMobileUpload");
+ vi.mocked(DocumentPicker.getDocumentAsync).mockResolvedValueOnce({
+ assets: [
+ {
+ mimeType: "video/mp4",
+ name: "launch-review.mp4",
+ size: 12_400_000,
+ uri: "file:///tmp/launch-review.mp4",
+ },
+ ],
+ canceled: false,
+ } as Awaited>);
+ vi.mocked(runMobileUpload).mockResolvedValueOnce({
+ id: "video_123",
+ } as Awaited>);
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ if (!browseButton) throw new Error("Browse Files button was not rendered");
+
+ await act(async () => {
+ await browseButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ expect(uploadQueueActionsState.value).toContainEqual(
+ expect.objectContaining({
+ progress: 0,
+ type: "processing",
+ }),
+ );
+ expect(
+ uploadQueueActionsState.value.some(
+ (action) =>
+ typeof action === "object" &&
+ action !== null &&
+ "type" in action &&
+ action.type === "fail",
+ ),
+ ).toBe(false);
+ });
+
+ it("completes uploaded files when the final processing refresh fails", async () => {
+ vi.useFakeTimers();
+ try {
+ const auth = createAuth();
+ auth.refresh = vi.fn(() => Promise.reject(new Error("Refresh failed")));
+ auth.client.getCap = vi.fn(() =>
+ Promise.resolve({
+ cap: {
+ upload: null,
+ },
+ }),
+ );
+ authState.value = auth;
+ const DocumentPicker = await import("expo-document-picker");
+ const { runMobileUpload } = await import("@/uploads/runMobileUpload");
+ vi.mocked(DocumentPicker.getDocumentAsync).mockResolvedValueOnce({
+ assets: [
+ {
+ mimeType: "video/mp4",
+ name: "launch-review.mp4",
+ size: 12_400_000,
+ uri: "file:///tmp/launch-review.mp4",
+ },
+ ],
+ canceled: false,
+ } as Awaited>);
+ vi.mocked(runMobileUpload).mockResolvedValueOnce({
+ id: "video_123",
+ } as Awaited>);
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const [browseButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ if (!browseButton)
+ throw new Error("Browse Files button was not rendered");
+
+ await act(async () => {
+ await browseButton.props.onPress();
+ await Promise.resolve();
+ });
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(1500);
+ });
+
+ expect(auth.client.getCap).toHaveBeenCalledWith("video_123");
+ expect(auth.refresh).toHaveBeenCalledTimes(2);
+ expect(uploadQueueActionsState.value).toContainEqual(
+ expect.objectContaining({
+ type: "complete",
+ }),
+ );
+ expect(
+ uploadQueueActionsState.value.some(
+ (action) =>
+ typeof action === "object" &&
+ action !== null &&
+ "type" in action &&
+ action.type === "fail",
+ ),
+ ).toBe(false);
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+
+ it("opens the native iOS upload queue sheet", async () => {
+ uploadQueueState.value.items = [
+ {
+ capId: null,
+ contentType: "video/mp4",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ error: "Network unavailable",
+ fileName: "failed-upload.mp4",
+ folderId: null,
+ id: "failed-upload",
+ localUri: "file:///tmp/failed-upload.mp4",
+ organizationId: "org_123",
+ progress: 0.42,
+ rawFileKey: null,
+ size: 124_000,
+ durationSeconds: 125,
+ status: "failed",
+ updatedAt: "2026-05-18T10:00:00.000Z",
+ },
+ ];
+ const renderer = await renderComponent(React.createElement(UploadScreen));
+ const tree = renderer.toJSON();
+ const [queueMenu] = renderer.root.findAllByProps({
+ accessibilityLabel: "More actions for failed-upload.mp4",
+ });
+ if (!queueMenu) throw new Error("Upload queue menu was not rendered");
+ const [queueRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "Upload failed-upload.mp4",
+ });
+ if (!queueRow) throw new Error("Upload queue row was not rendered");
+ expect(getTextNodes(tree).join("")).toContain(
+ "Upload failed ยท Network unavailable ยท 124 KB ยท 2 mins",
+ );
+ expect(queueMenu.props.hitSlop).toBe(6);
+ expect(queueMenu.props.accessibilityHint).toBe(
+ "Opens retry and remove actions",
+ );
+ expect(
+ hasProps(tree, {
+ accessibilityHint: "Upload failed. Opens upload actions",
+ accessibilityLabel: "Upload failed-upload.mp4",
+ accessibilityState: { busy: false, disabled: false },
+ }),
+ ).toBe(true);
+ expect(queueRow.props.accessibilityValue).toEqual({
+ text: "Upload failed ยท Network unavailable ยท 124 KB ยท 2 mins",
+ });
+ expect(
+ hasProps(tree, {
+ accessibilityLabel: "Upload progress for failed-upload.mp4",
+ accessibilityRole: "progressbar",
+ }),
+ ).toBe(false);
+ expect(hasProp(tree, "accessibilityRole", "alert")).toBe(true);
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+
+ const menuStopPropagation = vi.fn();
+ await act(async () => {
+ queueMenu.props.onPress({ stopPropagation: menuStopPropagation });
+ });
+
+ expect(menuStopPropagation).toHaveBeenCalled();
+ expect(showActionSheetWithOptions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cancelButtonIndex: 2,
+ destructiveButtonIndex: 1,
+ message: "Upload failed ยท Network unavailable ยท 124 KB ยท 2 mins",
+ options: ["Retry", "Remove from Queue", "Cancel"],
+ title: "failed-upload.mp4",
+ userInterfaceStyle: "light",
+ }),
+ expect.any(Function),
+ );
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ queueRow.props.onPress();
+ });
+
+ expect(showActionSheetWithOptions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cancelButtonIndex: 2,
+ destructiveButtonIndex: 1,
+ message: "Upload failed ยท Network unavailable ยท 124 KB ยท 2 mins",
+ options: ["Retry", "Remove from Queue", "Cancel"],
+ title: "failed-upload.mp4",
+ userInterfaceStyle: "light",
+ }),
+ expect.any(Function),
+ );
+ });
+
+ it("announces picker errors as native alerts", async () => {
+ const DocumentPicker = await import("expo-document-picker");
+ vi.mocked(DocumentPicker.getDocumentAsync).mockRejectedValueOnce(
+ new Error("Files unavailable"),
+ );
+ const uploadRenderer = await renderComponent(
+ React.createElement(UploadScreen),
+ );
+ const [browseButton] = uploadRenderer.root.findAllByProps({
+ accessibilityLabel: "Browse Files",
+ });
+ if (!browseButton) throw new Error("Browse Files button was not rendered");
+
+ await act(async () => {
+ await browseButton.props.onPress();
+ });
+
+ expect(getTextNodes(uploadRenderer.toJSON())).toContain(
+ "Upload source unavailable",
+ );
+ expect(getTextNodes(uploadRenderer.toJSON())).toContain(
+ "Files unavailable",
+ );
+ const [uploadSource] = uploadRenderer.root.findAllByProps({
+ accessibilityLabel: "Upload source unavailable",
+ });
+ if (!uploadSource) throw new Error("Upload source button was not rendered");
+ expect(uploadSource.props.accessibilityHint).toBe(
+ "Retries upload source options",
+ );
+ expect(uploadSource.props.accessibilityValue).toEqual({
+ text: "Files unavailable",
+ });
+ const [retryFilesButton] = uploadRenderer.root.findAllByProps({
+ accessibilityLabel: "Retry Files",
+ });
+ if (!retryFilesButton)
+ throw new Error("Retry Files button was not rendered");
+ expect(retryFilesButton.props.accessibilityHint).toBe("Files unavailable");
+ expect(retryFilesButton.props.disabled).toBe(false);
+ const [loomImport] = uploadRenderer.root.findAllByProps({
+ accessibilityLabel: "Open Loom import",
+ });
+ if (!loomImport) throw new Error("Loom import card was not rendered");
+ expect(loomImport.props.accessibilityValue).toBeUndefined();
+ expect(hasStyle(uploadRenderer.toJSON(), { color: "#e5484d" })).toBe(true);
+ expect(
+ hasProps(uploadRenderer.toJSON(), {
+ accessibilityLiveRegion: "polite",
+ accessibilityRole: "alert",
+ }),
+ ).toBe(true);
+ });
+
+ it("shows dashboard import actions", async () => {
+ const tree = await renderTree(React.createElement(CapsScreen));
+ const text = getTextNodes(tree);
+
+ expect(text).toContain("My Caps");
+ expect(text.filter((item) => item === "New Folder").length).toBeGreaterThan(
+ 0,
+ );
+ expect(text).not.toContain("Record");
+ expect(
+ text.filter((item) => item === "Import Video").length,
+ ).toBeGreaterThan(0);
+ expect(hasStyle(tree, { marginBottom: 40 })).toBe(true);
+ expect(text.join("")).toContain("Hey Richie! Import your first Cap");
+ expect(hasProp(tree, "accessibilityLabel", "Cap logo")).toBe(true);
+ expect(
+ hasProps(tree, {
+ accessibilityHint: "Opens import options",
+ accessibilityLabel: "Import Video",
+ }),
+ ).toBe(true);
+ });
+
+ it("announces dashboard load errors with a retry action", async () => {
+ const auth = createAuth();
+ auth.client.listCaps = vi.fn(() =>
+ Promise.reject(new Error("Network unavailable")),
+ );
+ authState.value = auth;
+ const renderer = await renderComponent(React.createElement(CapsScreen));
+
+ await act(async () => {
+ await Promise.resolve();
+ });
+
+ const tree = renderer.toJSON();
+ const text = getTextNodes(tree);
+
+ expect(text).toContain("Unable to load Caps");
+ expect(text).toContain("Network unavailable");
+ expect(
+ hasProps(tree, {
+ accessibilityLabel: "Library error: Network unavailable",
+ accessibilityLiveRegion: "polite",
+ accessibilityRole: "alert",
+ }),
+ ).toBe(true);
+
+ const [retryButton] = renderer.root.findAllByProps({
+ accessibilityLabel: "Try again",
+ });
+ if (!retryButton)
+ throw new Error("Dashboard retry action was not rendered");
+ expect(retryButton.props.accessibilityHint).toBe(
+ "Reloads your Cap library",
+ );
+
+ await act(async () => {
+ await retryButton.props.onPress();
+ await Promise.resolve();
+ });
+
+ expect(auth.client.listCaps).toHaveBeenCalledTimes(2);
+ });
+
+ it("renders dashboard folders with native folder rows", async () => {
+ const auth = createAuth();
+ auth.client.listCaps = () =>
+ Promise.resolve({
+ caps: [],
+ folders: [
+ {
+ color: "blue",
+ id: "folder_123",
+ name: "Product",
+ parentId: null,
+ videoCount: 2,
+ },
+ ],
+ pagination: {
+ hasNextPage: false,
+ page: 1,
+ totalPages: 1,
+ },
+ rootFolders: [],
+ });
+ authState.value = auth;
+
+ const renderer = await renderComponent(React.createElement(CapsScreen));
+ await act(async () => {
+ await Promise.resolve();
+ });
+ const tree = renderer.toJSON();
+ const text = getTextNodes(tree);
+
+ expect(text).toContain("Folders");
+ expect(text).toContain("Product");
+ expect(text.join("")).toContain("2 videos");
+ expect(hasStyle(tree, { paddingBottom: 24 })).toBe(true);
+ expect(hasProp(tree, "accessibilityLabel", "Open folder Product")).toBe(
+ true,
+ );
+
+ const [folderRow] = renderer.root.findAllByProps({
+ accessibilityLabel: "Open folder Product",
+ });
+ if (!folderRow) throw new Error("Folder row was not rendered");
+
+ expect(resolveStyle(folderRow.props.style)).toMatchObject({
+ backgroundColor: "#f0f0f0",
+ borderColor: "#e0e0e0",
+ });
+ expect(resolveStyle(folderRow.props.style, true)).toMatchObject({
+ backgroundColor: "#e8e8e8",
+ borderColor: "#d9d9d9",
+ });
+
+ await act(async () => {
+ folderRow.props.onPress();
+ });
+
+ expect(
+ hasProp(renderer.toJSON(), "accessibilityLabel", "Back to My Caps"),
+ ).toBe(true);
+ });
+
+ it("marks the dashboard card sharing action busy while visibility is updating", async () => {
+ const auth = createAuth();
+ const cap = {
+ commentCount: 2,
+ createdAt: "2026-05-18T10:00:00.000Z",
+ durationSeconds: 125,
+ folderId: null,
+ id: "video_123",
+ ownerName: "Richie",
+ protected: false,
+ public: true,
+ reactionCount: 3,
+ shareUrl: "https://cap.so/s/video_123",
+ thumbnailUrl: null,
+ title: "Launch review",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ upload: null,
+ viewCount: 7,
+ };
+ const sharingDeferred = createDeferred();
+ auth.client.listCaps = vi.fn(() =>
+ Promise.resolve({
+ caps: [cap],
+ folders: [],
+ pagination: {
+ hasNextPage: false,
+ page: 1,
+ totalPages: 1,
+ },
+ rootFolders: [],
+ }),
+ );
+ auth.client.updateCapSharing = vi.fn(() => sharingDeferred.promise);
+ authState.value = auth;
+
+ const renderer = await renderComponent(React.createElement(CapsScreen));
+ await act(async () => {
+ await Promise.resolve();
+ });
+ const [capCard] = renderer.root.findAllByProps({ cap });
+ if (!capCard) throw new Error("Cap card was not rendered");
+
+ const { ActionSheetIOS } = await import("react-native");
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ capCard.props.onVisibilityPress();
+ });
+
+ const [, sharingCallback] = showActionSheetWithOptions.mock.calls[0] ?? [];
+ if (!sharingCallback) throw new Error("Sharing callback was not set");
+
+ await act(async () => {
+ sharingCallback(0);
+ await Promise.resolve();
+ });
+
+ const [busyCard] = renderer.root.findAllByProps({ cap });
+ if (!busyCard) throw new Error("Busy Cap card was not rendered");
+
+ expect(auth.client.updateCapSharing).toHaveBeenCalledWith("video_123", {
+ public: false,
+ });
+ expect(busyCard.props.visibilityBusy).toBe(true);
+ expect(busyCard.props.visibilityDisabled).toBe(true);
+ expect(busyCard.props.visibilityDisabledHint).toBe(
+ "Sharing update is in progress",
+ );
+ expect(busyCard.props.visibilityValue).toBeUndefined();
+ expect(busyCard.props.visibilityAccessibilityValue).toBe(
+ "Updating sharing for Launch review",
+ );
+
+ await act(async () => {
+ sharingDeferred.resolve({ ...cap, public: false });
+ await sharingDeferred.promise;
+ await Promise.resolve();
+ });
+ });
+
+ it("opens the native iOS folder creation prompt and color sheet", async () => {
+ const auth = createAuth();
+ authState.value = auth;
+ const renderer = await renderComponent(React.createElement(CapsScreen));
+ const [newFolder] = renderer.root.findAllByProps({
+ accessibilityLabel: "New Folder",
+ });
+ if (!newFolder) throw new Error("New Folder action was not rendered");
+
+ const { ActionSheetIOS, Alert } = await import("react-native");
+ const prompt = vi.mocked(Alert.prompt);
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ prompt.mockClear();
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ newFolder.props.onPress();
+ });
+
+ expect(prompt).toHaveBeenCalledWith(
+ "New Folder",
+ "Name this folder.",
+ expect.any(Array),
+ "plain-text",
+ );
+
+ const buttons = prompt.mock.calls[0]?.[2] as
+ | Array<{ onPress?: (value?: string) => void }>
+ | undefined;
+ if (!Array.isArray(buttons)) {
+ throw new Error("Folder prompt buttons were not provided");
+ }
+ const nextButton = buttons[1];
+ const nextAction = nextButton?.onPress;
+ if (typeof nextAction !== "function") {
+ throw new Error("Folder prompt next action was not provided");
+ }
+
+ await act(async () => {
+ nextAction("Product");
+ });
+
+ expect(showActionSheetWithOptions).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cancelButtonIndex: 4,
+ message: "Product",
+ options: ["Normal", "Blue", "Red", "Yellow", "Cancel"],
+ title: "Folder color",
+ userInterfaceStyle: "light",
+ }),
+ expect.any(Function),
+ );
+
+ const [, colorCallback] = showActionSheetWithOptions.mock.calls[0] ?? [];
+ if (!colorCallback) throw new Error("Folder color callback was not set");
+
+ await act(async () => {
+ colorCallback(1);
+ await Promise.resolve();
+ });
+
+ expect(auth.client.createFolder).toHaveBeenCalledWith({
+ name: "Product",
+ color: "blue",
+ });
+ });
+
+ it("locks dashboard navigation while a folder is being created", async () => {
+ const auth = createAuth();
+ const folderDeferred =
+ createDeferred>>();
+ auth.client.createFolder = vi.fn(() => folderDeferred.promise);
+ authState.value = auth;
+ const renderer = await renderComponent(React.createElement(CapsScreen));
+ const [newFolder] = renderer.root.findAllByProps({
+ accessibilityLabel: "New Folder",
+ });
+ if (!newFolder) throw new Error("New Folder action was not rendered");
+
+ const { ActionSheetIOS, Alert } = await import("react-native");
+ const prompt = vi.mocked(Alert.prompt);
+ const showActionSheetWithOptions = vi.mocked(
+ ActionSheetIOS.showActionSheetWithOptions,
+ );
+ prompt.mockClear();
+ showActionSheetWithOptions.mockClear();
+
+ await act(async () => {
+ newFolder.props.onPress();
+ });
+
+ const buttons = prompt.mock.calls[0]?.[2] as
+ | Array<{ onPress?: (value?: string) => void }>
+ | undefined;
+ const nextAction = buttons?.[1]?.onPress;
+ if (typeof nextAction !== "function") {
+ throw new Error("Folder prompt next action was not provided");
+ }
+
+ await act(async () => {
+ nextAction("Product");
+ });
+
+ const [, colorCallback] = showActionSheetWithOptions.mock.calls[0] ?? [];
+ if (!colorCallback) throw new Error("Folder color callback was not set");
+
+ await act(async () => {
+ colorCallback(1);
+ await Promise.resolve();
+ });
+
+ const [creatingFolder] = renderer.root.findAllByProps({
+ accessibilityLabel: "New Folder",
+ });
+ if (!creatingFolder) {
+ throw new Error("Creating folder action was not rendered");
+ }
+ expect(getTextNodes(renderer.toJSON())).not.toContain("Creating...");
+ expect(creatingFolder.props.loading).toBe(true);
+ expect(creatingFolder.props.accessibilityHint).toBe(
+ "Folder creation is in progress",
+ );
+ expect(creatingFolder.props.accessibilityValue).toEqual({
+ text: "Creating folder Product",
+ });
+ for (const action of renderer.root.findAllByProps({
+ accessibilityLabel: "Import Video",
+ })) {
+ expect(action.props.disabled).toBe(true);
+ expect(action.props.accessibilityHint).toBe(
+ "Folder creation is in progress",
+ );
+ expect(action.props.accessibilityValue).toEqual({
+ text: "Creating folder Product",
+ });
+ }
+
+ await act(async () => {
+ folderDeferred.resolve({
+ id: "folder_123",
+ name: "Product",
+ color: "blue",
+ parentId: null,
+ videoCount: 0,
+ });
+ await folderDeferred.promise;
+ await Promise.resolve();
+ });
+ });
+});
diff --git a/apps/mobile/src/theme.test.ts b/apps/mobile/src/theme.test.ts
new file mode 100644
index 00000000000..13a0898d491
--- /dev/null
+++ b/apps/mobile/src/theme.test.ts
@@ -0,0 +1,63 @@
+import { describe, expect, it, vi } from "vitest";
+import { colors } from "./theme";
+
+vi.mock("react-native", () => ({
+ StyleSheet: {
+ create: >(styles: T) => styles,
+ },
+}));
+
+const webRadixColors = {
+ gray: {
+ gray1: "#fcfcfc",
+ gray2: "#f9f9f9",
+ gray3: "#f0f0f0",
+ gray4: "#e8e8e8",
+ gray5: "#e0e0e0",
+ gray6: "#d9d9d9",
+ gray7: "#cecece",
+ gray8: "#bbbbbb",
+ gray9: "#8d8d8d",
+ gray10: "#838383",
+ gray11: "#646464",
+ gray12: "#202020",
+ },
+ blue: {
+ blue1: "#fbfdff",
+ blue2: "#f4faff",
+ blue3: "#e6f4fe",
+ blue4: "#d5efff",
+ blue5: "#c2e5ff",
+ blue6: "#acd8fc",
+ blue7: "#8ec8f6",
+ blue8: "#5eb1ef",
+ blue9: "#0090ff",
+ blue10: "#0588f0",
+ blue11: "#0d74ce",
+ blue12: "#113264",
+ },
+ red: {
+ red1: "#fffcfc",
+ red2: "#fff7f7",
+ red3: "#feebec",
+ red4: "#ffdbdc",
+ red5: "#ffcdce",
+ red6: "#fdbdbe",
+ red7: "#f4a9aa",
+ red8: "#eb8e90",
+ red9: "#e5484d",
+ red10: "#dc3e42",
+ red11: "#ce2c31",
+ red12: "#641723",
+ },
+};
+
+describe("mobile theme", () => {
+ it("matches the Radix color scales imported by Cap web", () => {
+ expect(colors).toMatchObject({
+ ...webRadixColors.gray,
+ ...webRadixColors.blue,
+ ...webRadixColors.red,
+ });
+ });
+});
diff --git a/apps/mobile/src/theme.ts b/apps/mobile/src/theme.ts
new file mode 100644
index 00000000000..786269c6b82
--- /dev/null
+++ b/apps/mobile/src/theme.ts
@@ -0,0 +1,95 @@
+import { StyleSheet } from "react-native";
+
+export const colors = {
+ white: "#ffffff",
+ black: "#000000",
+ gray1: "#fcfcfc",
+ gray2: "#f9f9f9",
+ gray3: "#f0f0f0",
+ gray4: "#e8e8e8",
+ gray5: "#e0e0e0",
+ gray6: "#d9d9d9",
+ gray7: "#cecece",
+ gray8: "#bbbbbb",
+ gray9: "#8d8d8d",
+ gray10: "#838383",
+ gray11: "#646464",
+ gray12: "#202020",
+ appBackground: "#f9f9f9",
+ blue1: "#fbfdff",
+ blue2: "#f4faff",
+ blue3: "#e6f4fe",
+ blue4: "#d5efff",
+ blue5: "#c2e5ff",
+ blue6: "#acd8fc",
+ blue7: "#8ec8f6",
+ blue8: "#5eb1ef",
+ blue9: "#0090ff",
+ blue10: "#0588f0",
+ blue11: "#0d74ce",
+ blue12: "#113264",
+ red1: "#fffcfc",
+ red2: "#fff7f7",
+ red3: "#feebec",
+ red4: "#ffdbdc",
+ red5: "#ffcdce",
+ red6: "#fdbdbe",
+ red7: "#f4a9aa",
+ red8: "#eb8e90",
+ red9: "#e5484d",
+ red10: "#dc3e42",
+ red11: "#ce2c31",
+ red12: "#641723",
+ primary: "#005cb1",
+ primary2: "#004c93",
+ secondary: "#2eb4ff",
+ tertiary: "#c5eaff",
+ buttonBlue: "#2563eb",
+ buttonBlueHover: "#1d4ed8",
+ buttonBlueBorder: "#1e40af",
+ glass: "rgba(252, 252, 252, 0.72)",
+ blackAlpha5: "rgba(18, 22, 31, 0.05)",
+ blackAlpha10: "rgba(18, 22, 31, 0.1)",
+ blackAlpha40: "rgba(18, 22, 31, 0.4)",
+ blackAlpha60: "rgba(18, 22, 31, 0.6)",
+ green9: "#30a46c",
+ yellow3: "#fffab8",
+ yellow5: "#ffe770",
+ yellow9: "#f5d90a",
+};
+
+export const fonts = {
+ regular: "NeueMontreal-Regular",
+ medium: "NeueMontreal-Medium",
+ bold: "NeueMontreal-Bold",
+};
+
+export const radius = {
+ xs: 6,
+ sm: 8,
+ md: 12,
+ lg: 16,
+ xl: 20,
+ full: 999,
+};
+
+export const squircle = {
+ borderCurve: "continuous" as const,
+};
+
+export const shadows = StyleSheet.create({
+ card: {
+ shadowColor: colors.black,
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.04,
+ shadowRadius: 2,
+ elevation: 1,
+ },
+ popover: {
+ shadowColor: colors.black,
+ shadowOffset: { width: 0, height: 16 },
+ shadowOpacity: 0.12,
+ shadowRadius: 32,
+ elevation: 10,
+ },
+});
diff --git a/apps/mobile/src/uploads/fileTypes.test.ts b/apps/mobile/src/uploads/fileTypes.test.ts
new file mode 100644
index 00000000000..d253cfb319c
--- /dev/null
+++ b/apps/mobile/src/uploads/fileTypes.test.ts
@@ -0,0 +1,27 @@
+import { describe, expect, it } from "vitest";
+import { contentTypeForUpload, contentTypeFromName } from "./fileTypes";
+
+describe("mobile upload file type inference", () => {
+ it.each([
+ ["demo.mp4", "video/mp4"],
+ ["demo.mov", "video/quicktime"],
+ ["demo.webm", "video/webm"],
+ ["demo.mkv", "video/x-matroska"],
+ ["demo.avi", "video/x-msvideo"],
+ ["demo.m4v", "video/x-m4v"],
+ ])("infers %s as %s", (name, contentType) => {
+ expect(contentTypeFromName(name)).toBe(contentType);
+ });
+
+ it("keeps picker-provided video content types", () => {
+ expect(contentTypeForUpload("demo.mkv", "video/custom")).toBe(
+ "video/custom",
+ );
+ });
+
+ it("falls back to the filename when the picker returns an opaque type", () => {
+ expect(contentTypeForUpload("demo.mkv", "application/octet-stream")).toBe(
+ "video/x-matroska",
+ );
+ });
+});
diff --git a/apps/mobile/src/uploads/fileTypes.ts b/apps/mobile/src/uploads/fileTypes.ts
new file mode 100644
index 00000000000..b7539305617
--- /dev/null
+++ b/apps/mobile/src/uploads/fileTypes.ts
@@ -0,0 +1,24 @@
+const videoContentTypesByExtension: Record = {
+ avi: "video/x-msvideo",
+ m4v: "video/x-m4v",
+ mkv: "video/x-matroska",
+ mov: "video/quicktime",
+ mp4: "video/mp4",
+ webm: "video/webm",
+};
+
+const extensionFromName = (name: string) => {
+ const extension = name.split(".").at(-1)?.toLowerCase();
+ return extension && extension !== name.toLowerCase() ? extension : null;
+};
+
+export const contentTypeFromName = (name: string) =>
+ videoContentTypesByExtension[extensionFromName(name) ?? ""] ?? "video/mp4";
+
+export const contentTypeForUpload = (
+ name: string,
+ contentType?: string | null,
+) => {
+ if (contentType?.startsWith("video/")) return contentType;
+ return contentTypeFromName(name);
+};
diff --git a/apps/mobile/src/uploads/runMobileUpload.test.ts b/apps/mobile/src/uploads/runMobileUpload.test.ts
new file mode 100644
index 00000000000..3b664f1338c
--- /dev/null
+++ b/apps/mobile/src/uploads/runMobileUpload.test.ts
@@ -0,0 +1,173 @@
+import { Folder, Organisation, Video } from "@cap/web-domain";
+import { describe, expect, it, vi } from "vitest";
+import type { MobileApiClient, UploadFile } from "@/api/mobile";
+import { runMobileUpload } from "./runMobileUpload";
+
+const uploadMock = vi.hoisted(() => ({
+ uploadToTarget: vi.fn(
+ async (
+ _target: unknown,
+ _file: UploadFile,
+ onProgress?: (progress: { loaded: number; total: number }) => void,
+ ) => {
+ onProgress?.({ loaded: 40, total: 80 });
+ },
+ ),
+}));
+
+vi.mock("@/api/mobile", () => ({
+ uploadToTarget: uploadMock.uploadToTarget,
+}));
+
+describe("runMobileUpload", () => {
+ it("passes native video metadata through upload creation and retry-safe progress", async () => {
+ const createUpload = vi.fn(async () => ({
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ rawFileKey: "user_123/video_123/raw-upload.mov",
+ upload: {
+ type: "put" as const,
+ url: "https://uploads.example/video",
+ headers: {
+ "Content-Type": "video/quicktime",
+ },
+ },
+ cap: {
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "video",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:00:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: 12.5,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: false,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: null,
+ },
+ }));
+ const updateUploadProgress = vi.fn(async () => ({
+ success: true as const,
+ }));
+ const completeUpload = vi.fn(async () => ({ success: true as const }));
+ const client = {
+ createUpload,
+ updateUploadProgress,
+ completeUpload,
+ } as unknown as MobileApiClient;
+ const file: UploadFile = {
+ uri: "file:///tmp/video.mov",
+ name: "video.mov",
+ type: "video/quicktime",
+ size: 80,
+ durationSeconds: 12.5,
+ width: 1920,
+ height: 1080,
+ };
+ const onProgress = vi.fn();
+
+ await runMobileUpload({
+ client,
+ file,
+ organizationId: Organisation.OrganisationId.make("org_123"),
+ folderId: Folder.FolderId.make("folder_123"),
+ onProgress,
+ });
+
+ expect(createUpload).toHaveBeenCalledWith({
+ organizationId: "org_123",
+ folderId: "folder_123",
+ fileName: "video.mov",
+ contentType: "video/quicktime",
+ contentLength: 80,
+ durationSeconds: 12.5,
+ width: 1920,
+ height: 1080,
+ });
+ expect(updateUploadProgress).toHaveBeenCalledWith("video_123", {
+ uploaded: 40,
+ total: 80,
+ });
+ expect(completeUpload).toHaveBeenCalledWith("video_123", {
+ rawFileKey: "user_123/video_123/raw-upload.mov",
+ contentLength: 80,
+ });
+ expect(onProgress).toHaveBeenCalledWith(0.5);
+ });
+
+ it("normalizes non-finite native upload progress", async () => {
+ uploadMock.uploadToTarget.mockImplementationOnce(
+ async (
+ _target: unknown,
+ _file: UploadFile,
+ onProgress?: (progress: { loaded: number; total: number }) => void,
+ ) => {
+ onProgress?.({ loaded: Number.NaN, total: Number.NaN });
+ },
+ );
+ const createUpload = vi.fn(async () => ({
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ rawFileKey: "user_123/video_123/raw-upload.mov",
+ upload: {
+ type: "put" as const,
+ url: "https://uploads.example/video",
+ headers: {
+ "Content-Type": "video/quicktime",
+ },
+ },
+ cap: {
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "video",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:00:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: 12.5,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: false,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: null,
+ },
+ }));
+ const updateUploadProgress = vi.fn(async () => ({
+ success: true as const,
+ }));
+ const completeUpload = vi.fn(async () => ({ success: true as const }));
+ const client = {
+ createUpload,
+ updateUploadProgress,
+ completeUpload,
+ } as unknown as MobileApiClient;
+ const file: UploadFile = {
+ uri: "file:///tmp/video.mov",
+ name: "video.mov",
+ type: "video/quicktime",
+ size: 80,
+ durationSeconds: 12.5,
+ width: 1920,
+ height: 1080,
+ };
+ const onProgress = vi.fn();
+
+ await runMobileUpload({
+ client,
+ file,
+ onProgress,
+ });
+
+ expect(updateUploadProgress).toHaveBeenCalledWith("video_123", {
+ uploaded: 0,
+ total: 80,
+ });
+ expect(onProgress).toHaveBeenCalledWith(0);
+ });
+});
diff --git a/apps/mobile/src/uploads/runMobileUpload.ts b/apps/mobile/src/uploads/runMobileUpload.ts
new file mode 100644
index 00000000000..a295bc38803
--- /dev/null
+++ b/apps/mobile/src/uploads/runMobileUpload.ts
@@ -0,0 +1,71 @@
+import { Folder, Organisation } from "@cap/web-domain";
+import type { MobileApiClient, UploadFile } from "@/api/mobile";
+import { uploadToTarget } from "@/api/mobile";
+
+type RunMobileUploadInput = {
+ client: MobileApiClient;
+ file: UploadFile;
+ organizationId?: string | null;
+ folderId?: string | null;
+ onCreated?: (capId: string, rawFileKey: string) => void;
+ onProgress?: (progress: number) => void;
+};
+
+const nonNegativeFiniteNumber = (value: number | null | undefined) =>
+ typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
+
+const positiveFiniteNumber = (value: number | null | undefined) =>
+ typeof value === "number" && Number.isFinite(value) && value > 0
+ ? value
+ : null;
+
+const clampProgress = (progress: number) => {
+ const safeProgress = Number.isFinite(progress) ? progress : 0;
+ return Math.min(1, Math.max(0, safeProgress));
+};
+
+export const runMobileUpload = async ({
+ client,
+ file,
+ organizationId,
+ folderId,
+ onCreated,
+ onProgress,
+}: RunMobileUploadInput) => {
+ const created = await client.createUpload({
+ organizationId: organizationId
+ ? Organisation.OrganisationId.make(organizationId)
+ : undefined,
+ folderId: folderId ? Folder.FolderId.make(folderId) : undefined,
+ fileName: file.name,
+ contentType: file.type,
+ contentLength: file.size,
+ durationSeconds: file.durationSeconds,
+ width: file.width,
+ height: file.height,
+ });
+ onCreated?.(created.id, created.rawFileKey);
+
+ await uploadToTarget(created.upload, file, ({ loaded, total }) => {
+ const safeLoaded = nonNegativeFiniteNumber(loaded);
+ const safeTotal =
+ positiveFiniteNumber(total) ??
+ positiveFiniteNumber(file.size) ??
+ safeLoaded;
+ const progress = safeTotal > 0 ? safeLoaded / safeTotal : 0;
+ onProgress?.(clampProgress(progress));
+ client
+ .updateUploadProgress(created.id, {
+ uploaded: safeLoaded,
+ total: safeTotal,
+ })
+ .catch(() => {});
+ });
+
+ await client.completeUpload(created.id, {
+ rawFileKey: created.rawFileKey,
+ contentLength: file.size,
+ });
+
+ return created;
+};
diff --git a/apps/mobile/src/uploads/uploadQueue.test.ts b/apps/mobile/src/uploads/uploadQueue.test.ts
new file mode 100644
index 00000000000..aa446cad28e
--- /dev/null
+++ b/apps/mobile/src/uploads/uploadQueue.test.ts
@@ -0,0 +1,318 @@
+import { describe, expect, it } from "vitest";
+import {
+ emptyUploadQueue,
+ isTerminalUploadQueueAction,
+ uploadProgressPercent,
+ uploadQueueActionFromCapUpload,
+ uploadQueueReducer,
+ uploadQueueStatusText,
+} from "./uploadQueue";
+
+const item = {
+ id: "local-1",
+ localUri: "file:///tmp/video.mp4",
+ fileName: "video.mp4",
+ contentType: "video/mp4",
+ size: 100,
+ folderId: null,
+ organizationId: "org_123",
+ status: "queued" as const,
+ progress: 0,
+ error: null,
+ capId: null,
+ rawFileKey: null,
+ processingMessage: null,
+};
+
+describe("uploadQueueReducer", () => {
+ it("preserves failed uploads for retry", () => {
+ const queued = uploadQueueReducer(emptyUploadQueue, {
+ type: "enqueue",
+ item,
+ });
+ const failed = uploadQueueReducer(queued, {
+ type: "fail",
+ id: item.id,
+ error: "Network unavailable",
+ });
+ expect(failed.items[0]?.status).toBe("failed");
+ expect(failed.items[0]?.error).toBe("Network unavailable");
+
+ const retrying = uploadQueueReducer(failed, {
+ type: "retry",
+ id: item.id,
+ });
+ expect(retrying.items[0]?.status).toBe("queued");
+ expect(retrying.items[0]?.error).toBeNull();
+ expect(retrying.items[0]?.localUri).toBe(item.localUri);
+ });
+
+ it("clears stale server upload metadata before retrying", () => {
+ const queued = uploadQueueReducer(emptyUploadQueue, {
+ type: "enqueue",
+ item,
+ });
+ const uploading = uploadQueueReducer(queued, {
+ type: "start",
+ id: item.id,
+ capId: "cap_123",
+ rawFileKey: "raw/video.mp4",
+ });
+ const failed = uploadQueueReducer(uploading, {
+ type: "fail",
+ id: item.id,
+ error: "Upload target rejected the file",
+ });
+ const retrying = uploadQueueReducer(failed, {
+ type: "retry",
+ id: item.id,
+ });
+
+ expect(retrying.items[0]?.capId).toBeNull();
+ expect(retrying.items[0]?.rawFileKey).toBeNull();
+ });
+
+ it("keeps the created Cap id after upload completion", () => {
+ const queued = uploadQueueReducer(emptyUploadQueue, {
+ type: "enqueue",
+ item,
+ });
+ const uploading = uploadQueueReducer(queued, {
+ type: "start",
+ id: item.id,
+ capId: "cap_123",
+ rawFileKey: "raw/video.mp4",
+ });
+ const complete = uploadQueueReducer(uploading, {
+ type: "complete",
+ id: item.id,
+ });
+
+ expect(complete.items[0]).toMatchObject({
+ status: "complete",
+ capId: "cap_123",
+ rawFileKey: "raw/video.mp4",
+ });
+ });
+
+ it("uses the web finishing label while processing after upload", () => {
+ const queued = uploadQueueReducer(emptyUploadQueue, {
+ type: "enqueue",
+ item,
+ });
+ const uploading = uploadQueueReducer(queued, {
+ type: "start",
+ id: item.id,
+ capId: "cap_123",
+ rawFileKey: "raw/video.mp4",
+ });
+ const processing = uploadQueueReducer(uploading, {
+ type: "processing",
+ id: item.id,
+ progress: 0,
+ });
+
+ expect(processing.items[0]).toMatchObject({
+ status: "processing",
+ progress: 0,
+ capId: "cap_123",
+ rawFileKey: "raw/video.mp4",
+ });
+ expect(
+ processing.items[0] ? uploadQueueStatusText(processing.items[0]) : null,
+ ).toBe("Finishing up");
+ });
+
+ it("uses server processing progress and messages in the queue row", () => {
+ const queued = uploadQueueReducer(emptyUploadQueue, {
+ type: "enqueue",
+ item,
+ });
+ const processing = uploadQueueReducer(queued, {
+ type: "processing",
+ id: item.id,
+ progress: 0.42,
+ message: "Processing frames",
+ });
+
+ expect(processing.items[0]).toMatchObject({
+ status: "processing",
+ progress: 0.42,
+ processingMessage: "Processing frames",
+ });
+ expect(
+ processing.items[0] ? uploadQueueStatusText(processing.items[0]) : null,
+ ).toBe("Processing frames");
+ });
+
+ it("restores uploading status when progress arrives after processing", () => {
+ const queued = uploadQueueReducer(emptyUploadQueue, {
+ type: "enqueue",
+ item,
+ });
+ const processing = uploadQueueReducer(queued, {
+ type: "processing",
+ id: item.id,
+ progress: 0.25,
+ message: "Processing frames",
+ });
+ const uploading = uploadQueueReducer(processing, {
+ type: "progress",
+ id: item.id,
+ progress: 0.5,
+ });
+
+ expect(uploading.items[0]).toMatchObject({
+ status: "uploading",
+ progress: 0.5,
+ error: null,
+ processingMessage: null,
+ });
+ expect(
+ uploading.items[0] ? uploadQueueStatusText(uploading.items[0]) : null,
+ ).toBe("Uploading 50%");
+ });
+
+ it("keeps invalid queue progress display-safe", () => {
+ const queued = uploadQueueReducer(emptyUploadQueue, {
+ type: "enqueue",
+ item,
+ });
+ const invalidUploadProgress = uploadQueueReducer(queued, {
+ type: "progress",
+ id: item.id,
+ progress: Number.NaN,
+ });
+ const invalidProcessingProgress = uploadQueueReducer(queued, {
+ type: "processing",
+ id: item.id,
+ progress: Number.POSITIVE_INFINITY,
+ message: "Processing frames",
+ });
+
+ expect(invalidUploadProgress.items[0]).toMatchObject({
+ status: "uploading",
+ progress: 0,
+ });
+ expect(
+ invalidUploadProgress.items[0]
+ ? uploadQueueStatusText(invalidUploadProgress.items[0])
+ : null,
+ ).toBe("Uploading 0%");
+ expect(invalidProcessingProgress.items[0]).toMatchObject({
+ status: "processing",
+ progress: 0,
+ });
+ expect(uploadProgressPercent(Number.NaN)).toBe(0);
+ expect(uploadProgressPercent(Number.POSITIVE_INFINITY)).toBe(0);
+ });
+
+ it("maps settled server upload state back to local queue actions", () => {
+ expect(uploadQueueActionFromCapUpload(item.id, null)).toEqual({
+ type: "complete",
+ id: item.id,
+ });
+ expect(
+ uploadQueueActionFromCapUpload(item.id, {
+ uploaded: 100,
+ total: 100,
+ phase: "complete",
+ processingProgress: 100,
+ processingMessage: null,
+ processingError: null,
+ }),
+ ).toEqual({
+ type: "complete",
+ id: item.id,
+ });
+ expect(
+ uploadQueueActionFromCapUpload(item.id, {
+ uploaded: 100,
+ total: 100,
+ phase: "error",
+ processingProgress: 40,
+ processingMessage: null,
+ processingError: "Transcode failed",
+ }),
+ ).toEqual({
+ type: "fail",
+ id: item.id,
+ error: "Transcode failed",
+ });
+ });
+
+ it("maps active server upload state back to local queue progress", () => {
+ expect(
+ uploadQueueActionFromCapUpload(item.id, {
+ uploaded: 25,
+ total: 100,
+ phase: "uploading",
+ processingProgress: 0,
+ processingMessage: null,
+ processingError: null,
+ }),
+ ).toEqual({
+ type: "progress",
+ id: item.id,
+ progress: 0.25,
+ });
+ expect(
+ uploadQueueActionFromCapUpload(item.id, {
+ uploaded: 100,
+ total: 100,
+ phase: "processing",
+ processingProgress: 42,
+ processingMessage: "Processing frames",
+ processingError: null,
+ }),
+ ).toEqual({
+ type: "processing",
+ id: item.id,
+ progress: 0.42,
+ message: "Processing frames",
+ });
+ expect(
+ uploadQueueActionFromCapUpload(item.id, {
+ uploaded: 100,
+ total: 100,
+ phase: "generating_thumbnail",
+ processingProgress: 88,
+ processingMessage: null,
+ processingError: null,
+ }),
+ ).toEqual({
+ type: "processing",
+ id: item.id,
+ progress: 0.88,
+ message: "Finishing up",
+ });
+ });
+
+ it("keeps polling for non-terminal upload queue actions", () => {
+ expect(isTerminalUploadQueueAction({ type: "complete", id: item.id })).toBe(
+ true,
+ );
+ expect(
+ isTerminalUploadQueueAction({
+ type: "fail",
+ id: item.id,
+ error: "Transcode failed",
+ }),
+ ).toBe(true);
+ expect(
+ isTerminalUploadQueueAction({
+ type: "progress",
+ id: item.id,
+ progress: 0.25,
+ }),
+ ).toBe(false);
+ expect(
+ isTerminalUploadQueueAction({
+ type: "processing",
+ id: item.id,
+ progress: 0.42,
+ message: "Processing frames",
+ }),
+ ).toBe(false);
+ });
+});
diff --git a/apps/mobile/src/uploads/uploadQueue.ts b/apps/mobile/src/uploads/uploadQueue.ts
new file mode 100644
index 00000000000..5a3162f7596
--- /dev/null
+++ b/apps/mobile/src/uploads/uploadQueue.ts
@@ -0,0 +1,201 @@
+import type { MobileCapSummary } from "@/api/mobile";
+
+export type UploadQueueStatus =
+ | "queued"
+ | "uploading"
+ | "processing"
+ | "failed"
+ | "complete";
+
+export type UploadQueueItem = {
+ id: string;
+ localUri: string;
+ fileName: string;
+ contentType: string;
+ size: number;
+ durationSeconds?: number;
+ width?: number;
+ height?: number;
+ folderId: string | null;
+ organizationId: string | null;
+ status: UploadQueueStatus;
+ progress: number;
+ error: string | null;
+ capId: string | null;
+ rawFileKey: string | null;
+ processingMessage: string | null;
+ createdAt: string;
+ updatedAt: string;
+};
+
+export type UploadQueueAction =
+ | { type: "enqueue"; item: Omit }
+ | { type: "start"; id: string; capId: string; rawFileKey: string }
+ | { type: "progress"; id: string; progress: number }
+ | {
+ type: "processing";
+ id: string;
+ progress?: number;
+ message?: string | null;
+ }
+ | { type: "complete"; id: string }
+ | { type: "fail"; id: string; error: string }
+ | { type: "remove"; id: string }
+ | { type: "retry"; id: string };
+
+export type UploadQueueState = {
+ items: UploadQueueItem[];
+};
+
+const clampProgress = (progress: number) => {
+ if (!Number.isFinite(progress)) return 0;
+ return Math.min(1, Math.max(0, progress));
+};
+
+export const uploadProgressPercent = (progress: number) =>
+ Math.round(clampProgress(progress) * 100);
+
+export const isTerminalUploadQueueAction = (action: UploadQueueAction) =>
+ action.type === "complete" || action.type === "fail";
+
+export const uploadQueueStatusText = (item: UploadQueueItem) => {
+ switch (item.status) {
+ case "queued":
+ return "Queued";
+ case "uploading":
+ return `Uploading ${uploadProgressPercent(item.progress)}%`;
+ case "processing":
+ return item.processingMessage ?? "Finishing up";
+ case "complete":
+ return "Ready to view";
+ case "failed":
+ return "Upload failed";
+ }
+};
+
+export const uploadQueueActionFromCapUpload = (
+ id: string,
+ upload: MobileCapSummary["upload"],
+): UploadQueueAction | null => {
+ if (!upload || upload.phase === "complete") return { type: "complete", id };
+ if (upload.phase === "error") {
+ return {
+ type: "fail",
+ id,
+ error: upload.processingError ?? "Processing failed",
+ };
+ }
+ if (upload.phase === "uploading") {
+ return {
+ type: "progress",
+ id,
+ progress: upload.total > 0 ? upload.uploaded / upload.total : 0,
+ };
+ }
+ return {
+ type: "processing",
+ id,
+ progress: upload.processingProgress / 100,
+ message:
+ upload.processingMessage ??
+ (upload.phase === "processing" ? "Processing" : "Finishing up"),
+ };
+};
+
+const nowIso = () => new Date().toISOString();
+
+const updateItem = (
+ state: UploadQueueState,
+ id: string,
+ update: (item: UploadQueueItem) => UploadQueueItem,
+): UploadQueueState => ({
+ items: state.items.map((item) => (item.id === id ? update(item) : item)),
+});
+
+export const emptyUploadQueue: UploadQueueState = {
+ items: [],
+};
+
+export const uploadQueueReducer = (
+ state: UploadQueueState,
+ action: UploadQueueAction,
+): UploadQueueState => {
+ const updatedAt = nowIso();
+
+ switch (action.type) {
+ case "enqueue":
+ return {
+ items: [
+ ...state.items,
+ {
+ ...action.item,
+ createdAt: updatedAt,
+ updatedAt,
+ },
+ ],
+ };
+ case "start":
+ return updateItem(state, action.id, (item) => ({
+ ...item,
+ status: "uploading",
+ progress: 0,
+ error: null,
+ capId: action.capId,
+ rawFileKey: action.rawFileKey,
+ processingMessage: null,
+ updatedAt,
+ }));
+ case "progress":
+ return updateItem(state, action.id, (item) => ({
+ ...item,
+ status: "uploading",
+ progress: clampProgress(action.progress),
+ error: null,
+ processingMessage: null,
+ updatedAt,
+ }));
+ case "processing":
+ return updateItem(state, action.id, (item) => ({
+ ...item,
+ status: "processing",
+ progress:
+ action.progress !== undefined
+ ? clampProgress(action.progress)
+ : item.progress,
+ processingMessage: action.message ?? null,
+ updatedAt,
+ }));
+ case "complete":
+ return updateItem(state, action.id, (item) => ({
+ ...item,
+ status: "complete",
+ progress: 1,
+ error: null,
+ processingMessage: null,
+ updatedAt,
+ }));
+ case "fail":
+ return updateItem(state, action.id, (item) => ({
+ ...item,
+ status: "failed",
+ error: action.error,
+ processingMessage: null,
+ updatedAt,
+ }));
+ case "remove":
+ return {
+ items: state.items.filter((item) => item.id !== action.id),
+ };
+ case "retry":
+ return updateItem(state, action.id, (item) => ({
+ ...item,
+ status: "queued",
+ progress: 0,
+ error: null,
+ capId: null,
+ rawFileKey: null,
+ processingMessage: null,
+ updatedAt,
+ }));
+ }
+};
diff --git a/apps/mobile/src/utils/format.test.ts b/apps/mobile/src/utils/format.test.ts
new file mode 100644
index 00000000000..1ef39850004
--- /dev/null
+++ b/apps/mobile/src/utils/format.test.ts
@@ -0,0 +1,35 @@
+import { describe, expect, it } from "vitest";
+import { formatDuration, formatFileSize, formatRelativeDate } from "./format";
+
+describe("mobile formatters", () => {
+ it("formats card dates like Cap web", () => {
+ const now = new Date("2026-05-18T11:00:00.000Z");
+
+ expect(formatRelativeDate("2026-05-18T10:30:00.000Z", now)).toBe(
+ "30 minutes ago",
+ );
+ expect(formatRelativeDate("2026-05-18T09:45:00.000Z", now)).toBe(
+ "an hour ago",
+ );
+ expect(formatRelativeDate("2026-05-16T11:00:00.000Z", now)).toBe(
+ "2 days ago",
+ );
+ });
+
+ it("formats card durations like Cap web thumbnails", () => {
+ expect(formatDuration(0)).toBe("< 1 sec");
+ expect(formatDuration(8)).toBe("8 secs");
+ expect(formatDuration(61)).toBe("1 min");
+ expect(formatDuration(125)).toBe("2 mins");
+ expect(formatDuration(7200)).toBe("2 hrs");
+ });
+
+ it("formats native upload file sizes", () => {
+ expect(formatFileSize(null)).toBeNull();
+ expect(formatFileSize(0)).toBeNull();
+ expect(formatFileSize(640)).toBe("640 B");
+ expect(formatFileSize(124_000)).toBe("124 KB");
+ expect(formatFileSize(12_400_000)).toBe("12 MB");
+ expect(formatFileSize(2_300_000_000)).toBe("2 GB");
+ });
+});
diff --git a/apps/mobile/src/utils/format.ts b/apps/mobile/src/utils/format.ts
new file mode 100644
index 00000000000..a8b43469f65
--- /dev/null
+++ b/apps/mobile/src/utils/format.ts
@@ -0,0 +1,51 @@
+export const formatRelativeDate = (input: string, now = new Date()) => {
+ const date = new Date(input);
+ const diffMs = now.getTime() - date.getTime();
+ const diffSeconds = Math.max(0, Math.round(diffMs / 1000));
+ if (diffSeconds < 45) return "a few seconds ago";
+ if (diffSeconds < 90) return "a minute ago";
+
+ const diffMinutes = Math.round(diffSeconds / 60);
+ if (diffMinutes < 45) return `${diffMinutes} minutes ago`;
+ if (diffMinutes < 90) return "an hour ago";
+
+ const diffHours = Math.round(diffMinutes / 60);
+ if (diffHours < 22) return `${diffHours} hours ago`;
+ if (diffHours < 36) return "a day ago";
+
+ const diffDays = Math.round(diffHours / 24);
+ if (diffDays < 26) return `${diffDays} days ago`;
+ if (diffDays < 45) return "a month ago";
+
+ const diffMonths = Math.round(diffDays / 30);
+ if (diffDays < 320) return `${diffMonths} months ago`;
+ if (diffDays < 548) return "a year ago";
+
+ const diffYears = Math.round(diffDays / 365);
+ return `${diffYears} years ago`;
+};
+
+export const formatDuration = (seconds: number | null) => {
+ if (seconds === null || !Number.isFinite(seconds)) return null;
+ const safeSeconds = Math.max(0, Math.ceil(seconds));
+ const hours = Math.floor(safeSeconds / 3600);
+ const minutes = Math.floor(safeSeconds / 60);
+ const remainingSeconds = safeSeconds % 60;
+ if (hours > 0) return `${hours} hr${hours > 1 ? "s" : ""}`;
+ if (minutes > 0) return `${minutes} min${minutes > 1 ? "s" : ""}`;
+ if (remainingSeconds > 0) {
+ return `${remainingSeconds} sec${remainingSeconds === 1 ? "" : "s"}`;
+ }
+ return "< 1 sec";
+};
+
+export const formatFileSize = (bytes: number | null | undefined) => {
+ if (bytes === null || bytes === undefined || !Number.isFinite(bytes)) {
+ return null;
+ }
+ if (bytes <= 0) return null;
+ if (bytes >= 1_000_000_000) return `${Math.round(bytes / 1_000_000_000)} GB`;
+ if (bytes >= 1_000_000) return `${Math.round(bytes / 1_000_000)} MB`;
+ if (bytes >= 1_000) return `${Math.round(bytes / 1_000)} KB`;
+ return `${Math.round(bytes)} B`;
+};
diff --git a/apps/mobile/tsconfig.json b/apps/mobile/tsconfig.json
new file mode 100644
index 00000000000..495df8487cb
--- /dev/null
+++ b/apps/mobile/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "extends": "expo/tsconfig.base",
+ "compilerOptions": {
+ "strict": true,
+ "allowImportingTsExtensions": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"],
+ "@modules/*": ["modules/*"]
+ },
+ "types": ["vitest/globals"]
+ },
+ "include": [
+ "app",
+ "src",
+ "modules",
+ "plugins",
+ "expo-env.d.ts",
+ "*.js",
+ ".expo/types/**/*.ts"
+ ]
+}
diff --git a/apps/mobile/vitest.config.ts b/apps/mobile/vitest.config.ts
new file mode 100644
index 00000000000..d60d49c3b9c
--- /dev/null
+++ b/apps/mobile/vitest.config.ts
@@ -0,0 +1,14 @@
+import { fileURLToPath } from "node:url";
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ esbuild: {
+ jsx: "automatic",
+ jsxImportSource: "react",
+ },
+ resolve: {
+ alias: {
+ "@": fileURLToPath(new URL("./src", import.meta.url)),
+ },
+ },
+});
diff --git a/apps/web/__tests__/unit/mobile-api-contract.test.ts b/apps/web/__tests__/unit/mobile-api-contract.test.ts
new file mode 100644
index 00000000000..c336c187c1a
--- /dev/null
+++ b/apps/web/__tests__/unit/mobile-api-contract.test.ts
@@ -0,0 +1,174 @@
+import { Folder, Mobile, Organisation, User, Video } from "@cap/web-domain";
+import { Schema } from "effect";
+import { describe, expect, it } from "vitest";
+
+describe("mobile API contract schemas", () => {
+ it("decodes bootstrap responses without exposing database rows", () => {
+ const decoded = Schema.decodeUnknownSync(Mobile.MobileBootstrapResponse)({
+ user: {
+ id: User.UserId.make("user_123"),
+ name: "Richie",
+ email: "richie@example.com",
+ imageUrl: null,
+ activeOrganizationId: Organisation.OrganisationId.make("org_123"),
+ },
+ organizations: [
+ {
+ id: Organisation.OrganisationId.make("org_123"),
+ name: "Cap",
+ iconUrl: null,
+ role: "owner",
+ },
+ ],
+ activeOrganizationId: Organisation.OrganisationId.make("org_123"),
+ rootFolders: [
+ {
+ id: Folder.FolderId.make("folder_123"),
+ name: "Product",
+ color: "blue",
+ parentId: null,
+ videoCount: 4,
+ },
+ ],
+ });
+
+ expect(decoded.user.email).toBe("richie@example.com");
+ expect(decoded.rootFolders[0]?.videoCount).toBe(4);
+ });
+
+ it("decodes auth provider availability", () => {
+ const decoded = Schema.decodeUnknownSync(Mobile.MobileAuthConfigResponse)({
+ googleAuthAvailable: true,
+ workosAuthAvailable: false,
+ });
+
+ expect(decoded.googleAuthAvailable).toBe(true);
+ expect(decoded.workosAuthAvailable).toBe(false);
+ });
+
+ it("accepts Google and WorkOS mobile session providers", () => {
+ expect(
+ Schema.decodeUnknownSync(Mobile.MobileSessionRequestParams)({
+ redirectUri: "cap://auth",
+ provider: "google",
+ }).provider,
+ ).toBe("google");
+ expect(
+ Schema.decodeUnknownSync(Mobile.MobileSessionRequestParams)({
+ redirectUri: "cap://auth",
+ provider: "workos",
+ organizationId: "org_123",
+ }).organizationId,
+ ).toBe("org_123");
+ });
+
+ it("decodes Cap sharing visibility updates", () => {
+ const decoded = Schema.decodeUnknownSync(Mobile.MobileCapSharingInput)({
+ public: false,
+ });
+
+ expect(decoded.public).toBe(false);
+ });
+
+ it("decodes Cap title updates", () => {
+ const decoded = Schema.decodeUnknownSync(Mobile.MobileCapTitleInput)({
+ title: "Roadmap review",
+ });
+
+ expect(decoded.title).toBe("Roadmap review");
+ });
+
+ it("decodes Cap password updates", () => {
+ expect(
+ Schema.decodeUnknownSync(Mobile.MobileCapPasswordInput)({
+ password: "secret",
+ }).password,
+ ).toBe("secret");
+ expect(
+ Schema.decodeUnknownSync(Mobile.MobileCapPasswordInput)({
+ password: null,
+ }).password,
+ ).toBeNull();
+ });
+
+ it("decodes mobile folder creation inputs", () => {
+ const decoded = Schema.decodeUnknownSync(Mobile.MobileFolderCreateInput)({
+ name: "Product",
+ color: "blue",
+ });
+
+ expect(decoded).toEqual({
+ name: "Product",
+ color: "blue",
+ });
+ });
+
+ it("requires mobile caps dates to be serialized strings", () => {
+ expect(() =>
+ Schema.decodeUnknownSync(Mobile.MobileCapSummary)({
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: new Date("2026-05-18T10:00:00.000Z"),
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: 125,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: false,
+ viewCount: 7,
+ commentCount: 2,
+ reactionCount: 3,
+ upload: null,
+ }),
+ ).toThrow();
+ });
+
+ it("decodes signed playback and upload targets", () => {
+ const playback = Schema.decodeUnknownSync(Mobile.MobilePlaybackResponse)({
+ kind: "mp4",
+ url: "https://signed.example/video.mp4",
+ transcriptUrl: "https://signed.example/transcript.vtt",
+ });
+ const upload = Schema.decodeUnknownSync(Mobile.MobileUploadCreateResponse)({
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ rawFileKey: "user_123/video_123/raw-upload.mp4",
+ upload: {
+ type: "put",
+ url: "https://signed.example/upload",
+ headers: {
+ "Content-Type": "video/mp4",
+ },
+ },
+ cap: {
+ id: Video.VideoId.make("video_123"),
+ shareUrl: "https://cap.so/s/video_123",
+ title: "Launch review",
+ createdAt: "2026-05-18T10:00:00.000Z",
+ updatedAt: "2026-05-18T10:30:00.000Z",
+ ownerName: "Richie",
+ durationSeconds: null,
+ thumbnailUrl: null,
+ folderId: null,
+ public: true,
+ protected: false,
+ viewCount: 0,
+ commentCount: 0,
+ reactionCount: 0,
+ upload: {
+ uploaded: 0,
+ total: 0,
+ phase: "uploading",
+ processingProgress: 0,
+ processingMessage: null,
+ processingError: null,
+ },
+ },
+ });
+
+ expect(playback.url).toContain("signed.example");
+ expect(upload.upload.type).toBe("put");
+ });
+});
diff --git a/apps/web/app/(org)/login/form.tsx b/apps/web/app/(org)/login/form.tsx
index 50ffff2733f..81008579e4c 100644
--- a/apps/web/app/(org)/login/form.tsx
+++ b/apps/web/app/(org)/login/form.tsx
@@ -15,7 +15,7 @@ import Image from "next/image";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { signIn } from "next-auth/react";
-import { Suspense, useEffect, useState } from "react";
+import { Suspense, useCallback, useEffect, useId, useState } from "react";
import { toast } from "sonner";
import { getOrganizationSSOData } from "@/actions/organization/get-organization-sso-data";
import { trackEvent } from "@/app/utils/analytics";
@@ -43,10 +43,7 @@ export function LoginForm() {
const theme = Cookies.get("theme") || "light";
useEffect(() => {
- theme === "dark"
- ? (document.body.className = "dark")
- : (document.body.className = "light");
- //remove the dark mode when we leave the dashboard
+ document.body.className = theme === "dark" ? "dark" : "light";
return () => {
document.body.className = "light";
};
@@ -104,7 +101,7 @@ export function LoginForm() {
}
}, [emailSent]);
- const handleGoogleSignIn = () => {
+ const handleGoogleSignIn = useCallback(() => {
trackEvent("auth_started", {
method: "google",
is_signup: false,
@@ -113,7 +110,50 @@ export function LoginForm() {
signIn("google", {
...(next && next.length > 0 ? { callbackUrl: next } : {}),
});
- };
+ }, [next]);
+
+ const handleWorkosSignIn = useCallback(
+ async (orgId: string) => {
+ const data = await getOrganizationSSOData(
+ Organisation.OrganisationId.make(orgId),
+ );
+ setOrganizationName(data.name);
+
+ signIn(
+ "workos",
+ next && next.length > 0 ? { callbackUrl: next } : undefined,
+ {
+ organization: data.organizationId,
+ connection: data.connectionId,
+ },
+ );
+ },
+ [next],
+ );
+
+ useEffect(() => {
+ if (searchParams?.get("mobileProvider") === "google") {
+ handleGoogleSignIn();
+ }
+
+ if (searchParams?.get("mobileProvider") !== "workos") return;
+ const mobileOrganizationId = searchParams.get("organizationId");
+ if (!mobileOrganizationId) {
+ setShowOrgInput(true);
+ return;
+ }
+
+ let active = true;
+ handleWorkosSignIn(mobileOrganizationId).catch(() => {
+ if (!active) return;
+ setOrganizationId(mobileOrganizationId);
+ setShowOrgInput(true);
+ toast.error("Organization not found or SSO not configured");
+ });
+ return () => {
+ active = false;
+ };
+ }, [handleGoogleSignIn, handleWorkosSignIn, searchParams]);
const handleOrganizationLookup = async (e: React.FormEvent) => {
e.preventDefault();
@@ -123,15 +163,7 @@ export function LoginForm() {
}
try {
- const data = await getOrganizationSSOData(
- Organisation.OrganisationId.make(organizationId),
- );
- setOrganizationName(data.name);
-
- signIn("workos", undefined, {
- organization: data.organizationId,
- connection: data.connectionId,
- });
+ await handleWorkosSignIn(organizationId);
} catch (error) {
console.error("Lookup Error:", error);
toast.error("Organization not found or SSO not configured");
@@ -372,6 +404,8 @@ const LoginWithSSO = ({
setOrganizationId: (organizationId: string) => void;
organizationName: string | null;
}) => {
+ const organizationIdInputId = useId();
+
return (
setOrganizationId(e.target.value)}
@@ -415,12 +449,13 @@ const NormalLogin = ({
handleGoogleSignIn: () => void;
}) => {
const publicEnv = usePublicEnv();
+ const emailInputId = useId();
return (
value.toISOString();
+
+const normalizeEmail = (email: string) => email.trim().toLowerCase();
+
+const hashEmailCode = (code: string) =>
+ crypto
+ .createHash("sha256")
+ .update(`${code}${serverEnv().NEXTAUTH_SECRET}`)
+ .digest("hex");
+
+const sendMobileEmailCode = async (email: string, code: string) => {
+ if (!serverEnv().RESEND_API_KEY) {
+ console.log("");
+ console.log("Cap mobile verification code");
+ console.log(`Email: ${email}`);
+ console.log(`Code: ${code}`);
+ console.log("Expires in: 10 minutes");
+ console.log("");
+ return;
+ }
+
+ await sendEmail({
+ email,
+ subject: "Your Cap Verification Code",
+ react: OTPEmail({ code, email }),
+ });
+};
+
+const getEmailAuthAdapter = () => {
+ const adapter = authOptions().adapter;
+ const { createUser, getUserByEmail, updateUser } = adapter ?? {};
+
+ if (!createUser || !getUserByEmail || !updateUser) {
+ throw new Error("Email auth adapter is not configured");
+ }
+
+ return { createUser, getUserByEmail, updateUser };
+};
+
+const createOrUpdateEmailUser = async (email: string) => {
+ const { createUser, getUserByEmail, updateUser } = getEmailAuthAdapter();
+ const existingUser = await getUserByEmail(email);
+
+ if (existingUser) {
+ return updateUser({
+ id: existingUser.id,
+ emailVerified: new Date(),
+ });
+ }
+
+ return createUser({
+ email,
+ emailVerified: new Date(),
+ image: null,
+ name: null,
+ });
+};
+
+const parseBearerToken = (authorization: string | undefined) => {
+ if (!authorization) return null;
+ const [scheme, token] = authorization.split(" ");
+ if (scheme?.toLowerCase() !== "bearer" || !token) return null;
+ return token;
+};
+
+const parsePositiveInteger = (
+ value: string | undefined,
+ fallback: number,
+ max: number,
+) => {
+ const parsed = Number(value);
+ if (!Number.isFinite(parsed) || parsed < 1) return fallback;
+ return Math.min(Math.trunc(parsed), max);
+};
+
+const getMetadataRecord = (metadata: unknown): Record => {
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
+ return {};
+ }
+ return metadata as Record;
+};
+
+const getMetadataString = (metadata: Record, key: string) => {
+ const value = metadata[key];
+ return typeof value === "string" && value.length > 0 ? value : null;
+};
+
+const getMetadataChapters = (metadata: Record) => {
+ const chapters = metadata.chapters;
+ if (!Array.isArray(chapters)) return [];
+
+ return chapters.flatMap((chapter) => {
+ if (!chapter || typeof chapter !== "object" || Array.isArray(chapter)) {
+ return [];
+ }
+ const value = chapter as Record;
+ const title = value.title;
+ const start = value.start;
+ if (typeof title !== "string" || typeof start !== "number") return [];
+ return [{ title, start }];
+ });
+};
+
+const getDeploymentOrigin = () => {
+ const webUrl = serverEnv().WEB_URL;
+ const vercelEnv = serverEnv().VERCEL_ENV;
+
+ if (!vercelEnv || vercelEnv === "production") return webUrl;
+
+ if (vercelEnv === "preview") {
+ const branchHost = serverEnv().VERCEL_BRANCH_URL_HOST;
+ if (branchHost?.endsWith(".vercel.app")) return `https://${branchHost}`;
+ }
+
+ return webUrl;
+};
+
+const getFileExtension = (input: MobileUploadCreateInput) => {
+ const fileNameExtension = input.fileName.split(".").at(-1)?.toLowerCase();
+ if (
+ fileNameExtension &&
+ fileNameExtension !== input.fileName.toLowerCase() &&
+ /^[a-z0-9]+$/.test(fileNameExtension)
+ ) {
+ return fileNameExtension;
+ }
+
+ if (input.contentType.includes("quicktime")) return "mov";
+ if (input.contentType.includes("webm")) return "webm";
+ if (input.contentType.includes("matroska")) return "mkv";
+ if (input.contentType.includes("x-msvideo")) return "avi";
+ if (input.contentType.includes("x-m4v")) return "m4v";
+ return "mp4";
+};
+
+const getUploadTitle = (fileName: string) => {
+ const title = fileName.replace(/\.[^/.]+$/, "").trim();
+ return title.length > 0 ? title : "Mobile Upload";
+};
+
+const toMobileCapSummary = (
+ row: CapRow,
+ thumbnailUrl: string | null,
+ viewCount: number,
+): MobileCapSummary => ({
+ id: row.id,
+ shareUrl: `${serverEnv().WEB_URL}/s/${row.id}`,
+ title: row.name,
+ createdAt: toIsoString(row.createdAt),
+ updatedAt: toIsoString(row.updatedAt),
+ ownerName: row.ownerName ?? "",
+ durationSeconds: row.duration,
+ thumbnailUrl,
+ folderId: row.folderId,
+ public: row.public,
+ protected: row.hasPassword,
+ viewCount,
+ commentCount: Number(row.commentCount),
+ reactionCount: Number(row.reactionCount),
+ upload: row.uploadVideoId
+ ? {
+ uploaded: Number(row.uploadUploaded ?? 0),
+ total: Number(row.uploadTotal ?? 0),
+ phase: row.uploadPhase ?? "uploading",
+ processingProgress: Number(row.processingProgress ?? 0),
+ processingMessage: row.processingMessage,
+ processingError: row.processingError,
+ }
+ : null,
+});
+
+const withMappedErrors = (effect: Effect.Effect) =>
+ effect.pipe(
+ Effect.catchTags({
+ DatabaseError: () => new HttpApiError.InternalServerError(),
+ NoSuchElementException: () => new HttpApiError.NotFound(),
+ PolicyDenied: () => new HttpApiError.Forbidden(),
+ S3Error: () => new HttpApiError.InternalServerError(),
+ StorageError: () => new HttpApiError.InternalServerError(),
+ UnknownException: () => new HttpApiError.InternalServerError(),
+ VerifyVideoPasswordError: () => new HttpApiError.Forbidden(),
+ VideoNotFoundError: () => new HttpApiError.NotFound(),
+ }),
+ );
+
+const ensureEmailSignInAllowed = Effect.fn("Mobile.ensureEmailSignInAllowed")(
+ function* (email: string) {
+ if (!emailPattern.test(email)) {
+ return yield* Effect.fail(new HttpApiError.BadRequest());
+ }
+
+ const allowedDomains = serverEnv().CAP_ALLOWED_SIGNUP_DOMAINS;
+ if (!allowedDomains) return;
+
+ const database = yield* Database;
+ const [existingUser] = yield* database.use((db) =>
+ db
+ .select({ id: Db.users.id })
+ .from(Db.users)
+ .where(eq(Db.users.email, email))
+ .limit(1),
+ );
+
+ if (!existingUser && !isEmailAllowedForSignup(email, allowedDomains)) {
+ return yield* Effect.fail(new HttpApiError.Forbidden());
+ }
+ },
+);
+
+const createMobileApiKey = Effect.fn("Mobile.createMobileApiKey")(function* (
+ userId: User.UserId,
+) {
+ const database = yield* Database;
+ const apiKey = crypto.randomUUID();
+ yield* database.use((db) =>
+ db.insert(Db.authApiKeys).values({
+ id: apiKey,
+ userId,
+ }),
+ );
+
+ return {
+ type: "api_key" as const,
+ apiKey,
+ userId,
+ };
+});
+
+const requestEmailSession = Effect.fn("Mobile.requestEmailSession")(function* (
+ rawEmail: string,
+) {
+ const email = normalizeEmail(rawEmail);
+ yield* ensureEmailSignInAllowed(email);
+
+ const code = crypto.randomInt(100000, 1000000).toString();
+ const token = hashEmailCode(code);
+ const expires = new Date(Date.now() + emailCodeTtlMs);
+ const database = yield* Database;
+
+ yield* database.use(async (db) => {
+ const [existingToken] = await db
+ .select({ identifier: Db.verificationTokens.identifier })
+ .from(Db.verificationTokens)
+ .where(eq(Db.verificationTokens.identifier, email))
+ .limit(1);
+
+ if (existingToken) {
+ await db
+ .update(Db.verificationTokens)
+ .set({ token, expires })
+ .where(eq(Db.verificationTokens.identifier, email));
+ return;
+ }
+
+ await db.insert(Db.verificationTokens).values({
+ identifier: email,
+ token,
+ expires,
+ });
+ });
+
+ yield* Effect.tryPromise({
+ try: () => sendMobileEmailCode(email, code),
+ catch: () => new HttpApiError.InternalServerError(),
+ });
+
+ return { success: true as const };
+});
+
+const verifyEmailSession = Effect.fn("Mobile.verifyEmailSession")(function* ({
+ email: rawEmail,
+ code: rawCode,
+}: (typeof Mobile.MobileEmailSessionVerifyInput)["Type"]) {
+ const email = normalizeEmail(rawEmail);
+ const code = rawCode.trim();
+
+ if (!emailPattern.test(email) || !emailCodePattern.test(code)) {
+ return yield* Effect.fail(new HttpApiError.BadRequest());
+ }
+
+ yield* ensureEmailSignInAllowed(email);
+
+ const database = yield* Database;
+ const token = hashEmailCode(code);
+ const [verificationToken] = yield* database.use((db) =>
+ db
+ .select()
+ .from(Db.verificationTokens)
+ .where(
+ and(
+ eq(Db.verificationTokens.identifier, email),
+ eq(Db.verificationTokens.token, token),
+ ),
+ )
+ .limit(1),
+ );
+
+ if (!verificationToken) {
+ return yield* Effect.fail(new HttpApiError.Forbidden());
+ }
+
+ yield* database.use((db) =>
+ db
+ .delete(Db.verificationTokens)
+ .where(
+ and(
+ eq(Db.verificationTokens.identifier, email),
+ eq(Db.verificationTokens.token, token),
+ ),
+ ),
+ );
+
+ if (verificationToken.expires.valueOf() < Date.now()) {
+ return yield* Effect.fail(new HttpApiError.Forbidden());
+ }
+
+ const user = yield* Effect.tryPromise({
+ try: () => createOrUpdateEmailUser(email),
+ catch: () => new HttpApiError.InternalServerError(),
+ });
+
+ return yield* createMobileApiKey(User.UserId.make(user.id));
+});
+
+const getAccessibleOrganizations = Effect.fn(
+ "Mobile.getAccessibleOrganizations",
+)(function* (userId: User.UserId) {
+ const database = yield* Database;
+ const imageUploads = yield* ImageUploads;
+
+ const rows = yield* database.use((db) =>
+ db
+ .select({
+ id: Db.organizations.id,
+ name: Db.organizations.name,
+ ownerId: Db.organizations.ownerId,
+ iconUrl: Db.organizations.iconUrl,
+ role: Db.organizationMembers.role,
+ })
+ .from(Db.organizations)
+ .leftJoin(
+ Db.organizationMembers,
+ and(
+ eq(Db.organizationMembers.organizationId, Db.organizations.id),
+ eq(Db.organizationMembers.userId, userId),
+ ),
+ )
+ .where(
+ and(
+ isNull(Db.organizations.tombstoneAt),
+ or(
+ eq(Db.organizations.ownerId, userId),
+ eq(Db.organizationMembers.userId, userId),
+ ),
+ ),
+ ),
+ );
+
+ return yield* Effect.forEach(
+ rows,
+ (row) =>
+ Effect.gen(function* () {
+ const role: MobileOrganization["role"] =
+ row.ownerId === userId ? "owner" : (row.role ?? "member");
+ const iconUrl = row.iconUrl
+ ? yield* imageUploads.resolveImageUrl(row.iconUrl)
+ : null;
+
+ return {
+ id: row.id,
+ name: row.name,
+ iconUrl,
+ role,
+ };
+ }),
+ { concurrency: 5 },
+ );
+});
+
+const getRootFolders = Effect.fn("Mobile.getRootFolders")(function* (
+ organizationId: Organisation.OrganisationId,
+) {
+ const user = yield* CurrentUser;
+ const database = yield* Database;
+
+ const rows = yield* database.use((db) =>
+ db
+ .select({
+ id: Db.folders.id,
+ name: Db.folders.name,
+ color: Db.folders.color,
+ parentId: Db.folders.parentId,
+ videoCount: sql`(
+ SELECT COUNT(*)
+ FROM ${Db.videos}
+ WHERE ${Db.videos.folderId} = ${Db.folders.id}
+ AND ${Db.videos.ownerId} = ${user.id}
+ AND ${Db.videos.orgId} = ${organizationId}
+ )`,
+ })
+ .from(Db.folders)
+ .where(
+ and(
+ eq(Db.folders.organizationId, organizationId),
+ eq(Db.folders.createdById, user.id),
+ isNull(Db.folders.parentId),
+ isNull(Db.folders.spaceId),
+ ),
+ ),
+ );
+
+ return rows satisfies MobileFolder[];
+});
+
+const assertOrganizationAccess = Effect.fn("Mobile.assertOrganizationAccess")(
+ function* (organizationId: Organisation.OrganisationId) {
+ const user = yield* CurrentUser;
+ const organizations = yield* getAccessibleOrganizations(user.id);
+ const hasAccess = organizations.some((org) => org.id === organizationId);
+ if (!hasAccess) return yield* Effect.fail(new HttpApiError.Forbidden());
+ },
+);
+
+const getBootstrap = Effect.fn("Mobile.getBootstrap")(function* () {
+ const user = yield* CurrentUser;
+ const database = yield* Database;
+ const imageUploads = yield* ImageUploads;
+
+ const [userRow] = yield* database.use((db) =>
+ db
+ .select({
+ id: Db.users.id,
+ name: Db.users.name,
+ email: Db.users.email,
+ image: Db.users.image,
+ activeOrganizationId: Db.users.activeOrganizationId,
+ })
+ .from(Db.users)
+ .where(eq(Db.users.id, user.id)),
+ );
+ if (!userRow) return yield* Effect.fail(new HttpApiError.Unauthorized());
+
+ const organizations = yield* getAccessibleOrganizations(user.id);
+ const activeOrganization =
+ organizations.find((org) => org.id === userRow.activeOrganizationId) ??
+ organizations[0] ??
+ null;
+ const activeOrganizationId = activeOrganization?.id ?? null;
+ const rootFolders = activeOrganizationId
+ ? yield* getRootFolders(activeOrganizationId)
+ : [];
+ const imageUrl = userRow.image
+ ? yield* imageUploads.resolveImageUrl(userRow.image)
+ : null;
+
+ return {
+ user: {
+ id: userRow.id,
+ name: userRow.name,
+ email: userRow.email,
+ imageUrl,
+ activeOrganizationId: activeOrganizationId ?? user.activeOrganizationId,
+ },
+ organizations,
+ activeOrganizationId,
+ rootFolders,
+ };
+});
+
+const getCapRows = Effect.fn("Mobile.getCapRows")(function* ({
+ folderId,
+ page,
+ limit,
+}: {
+ folderId: Folder.FolderId | null;
+ page: number;
+ limit: number;
+}) {
+ const user = yield* CurrentUser;
+ const database = yield* Database;
+ const offset = (page - 1) * limit;
+ const folderFilter = folderId
+ ? eq(Db.videos.folderId, folderId)
+ : isNull(Db.videos.folderId);
+ const whereClause = and(
+ eq(Db.videos.ownerId, user.id),
+ eq(Db.videos.orgId, user.activeOrganizationId),
+ folderFilter,
+ isNull(Db.organizations.tombstoneAt),
+ );
+
+ const [totalRow] = yield* database.use((db) =>
+ db
+ .select({ value: count() })
+ .from(Db.videos)
+ .leftJoin(Db.organizations, eq(Db.videos.orgId, Db.organizations.id))
+ .where(whereClause),
+ );
+
+ const rows = yield* database.use((db) =>
+ db
+ .select({
+ id: Db.videos.id,
+ name: Db.videos.name,
+ createdAt: Db.videos.createdAt,
+ updatedAt: Db.videos.updatedAt,
+ ownerName: Db.users.name,
+ duration: Db.videos.duration,
+ folderId: Db.videos.folderId,
+ public: Db.videos.public,
+ hasPassword: sql`${Db.videos.password} IS NOT NULL`.mapWith(
+ Boolean,
+ ),
+ commentCount: sql`COUNT(DISTINCT CASE WHEN ${Db.comments.type} = 'text' THEN ${Db.comments.id} END)`,
+ reactionCount: sql`COUNT(DISTINCT CASE WHEN ${Db.comments.type} = 'emoji' THEN ${Db.comments.id} END)`,
+ uploadVideoId: Db.videoUploads.videoId,
+ uploadUploaded: Db.videoUploads.uploaded,
+ uploadTotal: Db.videoUploads.total,
+ uploadPhase: Db.videoUploads.phase,
+ processingProgress: Db.videoUploads.processingProgress,
+ processingMessage: Db.videoUploads.processingMessage,
+ processingError: Db.videoUploads.processingError,
+ metadata: Db.videos.metadata,
+ transcriptionStatus: Db.videos.transcriptionStatus,
+ })
+ .from(Db.videos)
+ .leftJoin(Db.comments, eq(Db.videos.id, Db.comments.videoId))
+ .leftJoin(Db.users, eq(Db.videos.ownerId, Db.users.id))
+ .leftJoin(Db.videoUploads, eq(Db.videos.id, Db.videoUploads.videoId))
+ .leftJoin(Db.organizations, eq(Db.videos.orgId, Db.organizations.id))
+ .where(whereClause)
+ .groupBy(
+ Db.videos.id,
+ Db.videos.name,
+ Db.videos.createdAt,
+ Db.videos.updatedAt,
+ Db.users.name,
+ Db.videos.duration,
+ Db.videos.folderId,
+ Db.videos.public,
+ Db.videos.password,
+ Db.videoUploads.videoId,
+ Db.videoUploads.uploaded,
+ Db.videoUploads.total,
+ Db.videoUploads.phase,
+ Db.videoUploads.processingProgress,
+ Db.videoUploads.processingMessage,
+ Db.videoUploads.processingError,
+ Db.videos.metadata,
+ Db.videos.transcriptionStatus,
+ )
+ .orderBy(desc(Db.videos.effectiveCreatedAt))
+ .limit(limit)
+ .offset(offset),
+ );
+
+ return { rows, total: totalRow?.value ?? 0 };
+});
+
+const getCapsList = Effect.fn("Mobile.getCapsList")(function* (
+ params: (typeof Mobile.MobileCapsListParams)["Type"],
+) {
+ const page = parsePositiveInteger(params.page, 1, 10_000);
+ const limit = parsePositiveInteger(params.limit, 20, 50);
+ const folderId = params.folderId
+ ? Folder.FolderId.make(params.folderId)
+ : null;
+ const videos = yield* Videos;
+ const user = yield* CurrentUser;
+
+ const [{ rows, total }, folders] = yield* Effect.all([
+ getCapRows({ folderId, page, limit }),
+ folderId ? Effect.succeed([]) : getRootFolders(user.activeOrganizationId),
+ ]);
+ const analyticsExits = yield* videos
+ .getAnalyticsBulk(rows.map((row) => row.id))
+ .pipe(Effect.catchAll(() => Effect.succeed([])));
+ const viewCounts = new Map();
+
+ rows.forEach((row, index) => {
+ const result = analyticsExits[index];
+ viewCounts.set(
+ row.id,
+ result && Exit.isSuccess(result) ? result.value.count : 0,
+ );
+ });
+
+ const caps = yield* Effect.forEach(
+ rows,
+ (row) =>
+ videos.getThumbnailURL(row.id).pipe(
+ Effect.map(Option.getOrNull),
+ Effect.catchAll(() => Effect.succeed(null)),
+ Effect.map((thumbnailUrl) =>
+ toMobileCapSummary(row, thumbnailUrl, viewCounts.get(row.id) ?? 0),
+ ),
+ ),
+ { concurrency: 5 },
+ );
+
+ return {
+ folders,
+ caps,
+ page,
+ limit,
+ total,
+ hasMore: page * limit < total,
+ };
+});
+
+const createMobileFolder = Effect.fn("Mobile.createFolder")(function* (
+ input: MobileFolderCreateInput,
+) {
+ const user = yield* CurrentUser;
+ const name = input.name.trim();
+ if (!name) return yield* Effect.fail(new HttpApiError.BadRequest());
+
+ const organizationId = user.activeOrganizationId;
+ yield* assertOrganizationAccess(organizationId);
+
+ const color = input.color ?? "normal";
+ const id = Folder.FolderId.make(nanoId());
+ const database = yield* Database;
+
+ yield* database.use((db) =>
+ db.insert(Db.folders).values({
+ id,
+ name,
+ color,
+ organizationId,
+ createdById: user.id,
+ parentId: null,
+ spaceId: null,
+ }),
+ );
+
+ yield* Effect.sync(() => {
+ revalidatePath("/dashboard/caps");
+ });
+
+ return {
+ id,
+ name,
+ color,
+ parentId: null,
+ videoCount: 0,
+ };
+});
+
+const getCapById = Effect.fn("Mobile.getCapById")(function* (
+ videoId: Video.VideoId,
+) {
+ const user = yield* CurrentUser;
+ const database = yield* Database;
+ const videos = yield* Videos;
+
+ const [row] = yield* database.use((db) =>
+ db
+ .select({
+ id: Db.videos.id,
+ name: Db.videos.name,
+ createdAt: Db.videos.createdAt,
+ updatedAt: Db.videos.updatedAt,
+ ownerName: Db.users.name,
+ duration: Db.videos.duration,
+ folderId: Db.videos.folderId,
+ public: Db.videos.public,
+ hasPassword: sql`${Db.videos.password} IS NOT NULL`.mapWith(
+ Boolean,
+ ),
+ commentCount: sql`COUNT(DISTINCT CASE WHEN ${Db.comments.type} = 'text' THEN ${Db.comments.id} END)`,
+ reactionCount: sql`COUNT(DISTINCT CASE WHEN ${Db.comments.type} = 'emoji' THEN ${Db.comments.id} END)`,
+ uploadVideoId: Db.videoUploads.videoId,
+ uploadUploaded: Db.videoUploads.uploaded,
+ uploadTotal: Db.videoUploads.total,
+ uploadPhase: Db.videoUploads.phase,
+ processingProgress: Db.videoUploads.processingProgress,
+ processingMessage: Db.videoUploads.processingMessage,
+ processingError: Db.videoUploads.processingError,
+ metadata: Db.videos.metadata,
+ transcriptionStatus: Db.videos.transcriptionStatus,
+ })
+ .from(Db.videos)
+ .leftJoin(Db.comments, eq(Db.videos.id, Db.comments.videoId))
+ .leftJoin(Db.users, eq(Db.videos.ownerId, Db.users.id))
+ .leftJoin(Db.videoUploads, eq(Db.videos.id, Db.videoUploads.videoId))
+ .where(and(eq(Db.videos.id, videoId), eq(Db.videos.ownerId, user.id)))
+ .groupBy(
+ Db.videos.id,
+ Db.videos.name,
+ Db.videos.createdAt,
+ Db.videos.updatedAt,
+ Db.users.name,
+ Db.videos.duration,
+ Db.videos.folderId,
+ Db.videos.public,
+ Db.videos.password,
+ Db.videoUploads.videoId,
+ Db.videoUploads.uploaded,
+ Db.videoUploads.total,
+ Db.videoUploads.phase,
+ Db.videoUploads.processingProgress,
+ Db.videoUploads.processingMessage,
+ Db.videoUploads.processingError,
+ Db.videos.metadata,
+ Db.videos.transcriptionStatus,
+ ),
+ );
+
+ if (!row) return yield* Effect.fail(new HttpApiError.NotFound());
+
+ const thumbnailUrl = yield* videos.getThumbnailURL(row.id).pipe(
+ Effect.map(Option.getOrNull),
+ Effect.catchAll(() => Effect.succeed(null)),
+ );
+ const analytics = yield* videos.getAnalytics(row.id).pipe(
+ Effect.map((result) => result.count),
+ Effect.catchAll(() => Effect.succeed(0)),
+ );
+
+ return { row, cap: toMobileCapSummary(row, thumbnailUrl, analytics) };
+});
+
+const getComments = Effect.fn("Mobile.getComments")(function* (
+ videoId: Video.VideoId,
+) {
+ const database = yield* Database;
+ const imageUploads = yield* ImageUploads;
+
+ const rows = yield* database.use((db) =>
+ db
+ .select({
+ id: Db.comments.id,
+ videoId: Db.comments.videoId,
+ type: Db.comments.type,
+ content: Db.comments.content,
+ timestamp: Db.comments.timestamp,
+ parentCommentId: Db.comments.parentCommentId,
+ createdAt: Db.comments.createdAt,
+ updatedAt: Db.comments.updatedAt,
+ authorId: Db.comments.authorId,
+ authorName: Db.users.name,
+ authorImage: Db.users.image,
+ })
+ .from(Db.comments)
+ .leftJoin(Db.users, eq(Db.comments.authorId, Db.users.id))
+ .where(eq(Db.comments.videoId, videoId))
+ .orderBy(Db.comments.createdAt),
+ );
+
+ return yield* Effect.forEach(
+ rows,
+ (row) =>
+ Effect.gen(function* () {
+ const imageUrl = row.authorImage
+ ? yield* imageUploads
+ .resolveImageUrl(row.authorImage)
+ .pipe(Effect.catchAll(() => Effect.succeed(null)))
+ : null;
+
+ return {
+ id: row.id,
+ videoId: row.videoId,
+ type: row.type,
+ content: row.content,
+ timestamp: row.timestamp,
+ parentCommentId: row.parentCommentId,
+ createdAt: toIsoString(row.createdAt),
+ updatedAt: toIsoString(row.updatedAt),
+ author: {
+ id: row.authorId,
+ name: row.authorName,
+ imageUrl,
+ },
+ };
+ }),
+ { concurrency: 5 },
+ );
+});
+
+const getCapDetail = Effect.fn("Mobile.getCapDetail")(function* (
+ videoId: Video.VideoId,
+) {
+ const { row, cap } = yield* getCapById(videoId);
+ const metadata = getMetadataRecord(row.metadata);
+ const comments = yield* getComments(videoId);
+
+ return {
+ cap,
+ summary: getMetadataString(metadata, "summary"),
+ chapters: getMetadataChapters(metadata),
+ transcriptionStatus: row.transcriptionStatus,
+ comments,
+ shareUrl: `${serverEnv().WEB_URL}/s/${videoId}`,
+ };
+});
+
+const createMobileComment = Effect.fn("Mobile.createComment")(function* ({
+ videoId,
+ content,
+ timestamp,
+ parentCommentId,
+ type,
+}: {
+ videoId: Video.VideoId;
+ content: string;
+ timestamp: number | null;
+ parentCommentId: Comment.CommentId | null;
+ type: "text" | "emoji";
+}) {
+ const user = yield* CurrentUser;
+ yield* getCapById(videoId);
+
+ const trimmedContent = content.trim();
+ if (trimmedContent.length === 0) {
+ return yield* Effect.fail(new HttpApiError.BadRequest());
+ }
+
+ const id = Comment.CommentId.make(nanoId());
+ const now = new Date();
+ const database = yield* Database;
+ yield* database.use((db) =>
+ db.insert(Db.comments).values({
+ id,
+ authorId: user.id,
+ type,
+ content: trimmedContent,
+ videoId,
+ timestamp,
+ parentCommentId,
+ createdAt: now,
+ updatedAt: now,
+ }),
+ );
+
+ const notificationType = parentCommentId
+ ? "reply"
+ : type === "emoji"
+ ? "reaction"
+ : "comment";
+
+ yield* Effect.tryPromise(() =>
+ createNotification({
+ type: notificationType,
+ videoId,
+ authorId: user.id,
+ comment: { id, content: trimmedContent },
+ parentCommentId: parentCommentId ?? undefined,
+ }),
+ ).pipe(Effect.catchAll(() => Effect.void));
+
+ const comments = yield* getComments(videoId);
+ const created = comments.find((comment) => comment.id === id);
+ if (!created)
+ return yield* Effect.fail(new HttpApiError.InternalServerError());
+ return created;
+});
+
+const getPlayback = Effect.fn("Mobile.getPlayback")(function* (
+ videoId: Video.VideoId,
+) {
+ const videos = yield* Videos;
+ const storage = yield* Storage;
+ const [video] = yield* videos.getByIdForViewing(videoId).pipe(
+ Effect.flatten,
+ Effect.catchTag("NoSuchElementException", () => new Video.NotFoundError()),
+ );
+ const [bucket] = yield* storage.getAccessForVideo(video);
+ const source = Video.Video.getSource(video);
+
+ const transcriptKey = `${video.ownerId}/${video.id}/transcription.vtt`;
+ const transcriptUrl = yield* bucket.headObject(transcriptKey).pipe(
+ Effect.flatMap(() => bucket.getSignedObjectUrl(transcriptKey)),
+ Effect.catchAll(() => Effect.succeed(null)),
+ );
+
+ if (source instanceof Video.Mp4Source) {
+ const url = yield* bucket.getSignedObjectUrl(source.getFileKey());
+ return { kind: "mp4" as const, url, transcriptUrl };
+ }
+
+ if (source instanceof Video.M3U8Source) {
+ const url = yield* bucket.getSignedObjectUrl(source.getPlaylistFileKey());
+ return { kind: "hls" as const, url, transcriptUrl };
+ }
+
+ if (source instanceof Video.SegmentsSource) {
+ return {
+ kind: "hls" as const,
+ url: `${serverEnv().WEB_URL}/api/playlist?videoId=${video.id}&videoType=segments-master`,
+ transcriptUrl,
+ };
+ }
+
+ return yield* Effect.fail(new HttpApiError.NotFound());
+});
+
+const createUpload = Effect.fn("Mobile.createUpload")(function* (
+ input: MobileUploadCreateInput,
+) {
+ const user = yield* CurrentUser;
+ const organizationId = input.organizationId ?? user.activeOrganizationId;
+ yield* assertOrganizationAccess(organizationId);
+
+ if (!input.contentType.startsWith("video/")) {
+ return yield* Effect.fail(new HttpApiError.BadRequest());
+ }
+
+ const database = yield* Database;
+ const storage = yield* Storage;
+ const repo = yield* VideosRepo;
+ const folderId = input.folderId;
+
+ if (folderId) {
+ const [folder] = yield* database.use((db) =>
+ db
+ .select({ id: Db.folders.id })
+ .from(Db.folders)
+ .where(
+ and(
+ eq(Db.folders.id, folderId),
+ eq(Db.folders.organizationId, organizationId),
+ eq(Db.folders.createdById, user.id),
+ isNull(Db.folders.spaceId),
+ ),
+ ),
+ );
+ if (!folder) return yield* Effect.fail(new HttpApiError.NotFound());
+ }
+
+ const writable = yield* storage.getWritableAccessForUser(
+ user.id,
+ organizationId,
+ );
+ const videoId = yield* repo.create({
+ ownerId: user.id,
+ orgId: organizationId,
+ name: getUploadTitle(input.fileName),
+ public: serverEnv().CAP_VIDEOS_DEFAULT_PUBLIC,
+ source: { type: "webMP4" },
+ bucketId: writable.bucketId,
+ storageIntegrationId: writable.storageIntegrationId,
+ folderId: Option.fromNullable(folderId),
+ width: Option.fromNullable(input.width),
+ height: Option.fromNullable(input.height),
+ duration: Option.fromNullable(input.durationSeconds),
+ metadata: Option.none(),
+ transcriptionStatus: Option.none(),
+ });
+
+ yield* database.use((db) =>
+ db.insert(Db.videoUploads).values({
+ videoId,
+ total: input.contentLength ?? 0,
+ mode: "singlepart",
+ }),
+ );
+
+ const rawFileKey = `${user.id}/${videoId}/raw-upload.${getFileExtension(input)}`;
+ const upload = yield* writable.access.createUploadTarget(rawFileKey, {
+ contentType: input.contentType,
+ method: "put",
+ fields: {
+ "Content-Type": input.contentType,
+ "x-amz-meta-userid": user.id,
+ "x-amz-meta-source": "cap-mobile-ios",
+ },
+ });
+ const { cap } = yield* getCapById(videoId);
+
+ return {
+ id: videoId,
+ shareUrl: `${serverEnv().WEB_URL}/s/${videoId}`,
+ rawFileKey,
+ upload,
+ cap,
+ };
+});
+
+const ApiLive = HttpApiBuilder.api(Mobile.MobileApiContract).pipe(
+ Layer.provide(
+ HttpApiBuilder.group(Mobile.MobileApiContract, "mobile", (handlers) =>
+ Effect.gen(function* () {
+ const videos = yield* Videos;
+ const database = yield* Database;
+
+ return handlers
+ .handle("getAuthConfig", () =>
+ Effect.succeed({
+ googleAuthAvailable: Boolean(serverEnv().GOOGLE_CLIENT_ID),
+ workosAuthAvailable: Boolean(serverEnv().WORKOS_CLIENT_ID),
+ }),
+ )
+ .handle("requestSession", ({ request, urlParams }) =>
+ withMappedErrors(
+ Effect.gen(function* () {
+ const user = yield* getCurrentUser;
+ if (Option.isNone(user)) {
+ const redirectOrigin = getDeploymentOrigin();
+ const requestUrl = new URL(request.url);
+ const loginRedirectUrl = new URL(`${redirectOrigin}/login`);
+ loginRedirectUrl.searchParams.set(
+ "next",
+ new URL(
+ `${redirectOrigin}${requestUrl.pathname}${requestUrl.search}`,
+ ).toString(),
+ );
+ if (urlParams.provider === "google") {
+ loginRedirectUrl.searchParams.set(
+ "mobileProvider",
+ "google",
+ );
+ } else if (urlParams.provider === "workos") {
+ loginRedirectUrl.searchParams.set(
+ "mobileProvider",
+ "workos",
+ );
+ if (urlParams.organizationId) {
+ loginRedirectUrl.searchParams.set(
+ "organizationId",
+ urlParams.organizationId,
+ );
+ }
+ }
+ return HttpServerResponse.redirect(
+ loginRedirectUrl.toString(),
+ );
+ }
+
+ const session = yield* createMobileApiKey(user.value.id);
+
+ if (urlParams.redirectUri) {
+ const redirectUrl = new URL(urlParams.redirectUri);
+ redirectUrl.searchParams.set("api_key", session.apiKey);
+ redirectUrl.searchParams.set("user_id", user.value.id);
+ return HttpServerResponse.redirect(redirectUrl.toString());
+ }
+
+ return session;
+ }),
+ ),
+ )
+ .handle("requestEmailSession", ({ payload }) =>
+ withMappedErrors(requestEmailSession(payload.email)),
+ )
+ .handle("verifyEmailSession", ({ payload }) =>
+ withMappedErrors(verifyEmailSession(payload)),
+ )
+ .handle("revokeSession", ({ headers }) =>
+ withMappedErrors(
+ Effect.gen(function* () {
+ const token = parseBearerToken(headers.authorization);
+ if (!token)
+ return yield* Effect.fail(new HttpApiError.Unauthorized());
+ yield* database.use((db) =>
+ db.delete(Db.authApiKeys).where(eq(Db.authApiKeys.id, token)),
+ );
+ return { success: true as const };
+ }),
+ ),
+ )
+ .handle("bootstrap", () => withMappedErrors(getBootstrap()))
+ .handle("setActiveOrganization", ({ payload }) =>
+ withMappedErrors(
+ Effect.gen(function* () {
+ const user = yield* CurrentUser;
+ yield* assertOrganizationAccess(payload.organizationId);
+ yield* database.use((db) =>
+ db
+ .update(Db.users)
+ .set({ activeOrganizationId: payload.organizationId })
+ .where(eq(Db.users.id, user.id)),
+ );
+ return yield* getBootstrap();
+ }),
+ ),
+ )
+ .handle("listCaps", ({ urlParams }) =>
+ withMappedErrors(getCapsList(urlParams)),
+ )
+ .handle("createFolder", ({ payload }) =>
+ withMappedErrors(createMobileFolder(payload)),
+ )
+ .handle("getCap", ({ path }) =>
+ withMappedErrors(getCapDetail(path.id)),
+ )
+ .handle("updateCapSharing", ({ path, payload }) =>
+ withMappedErrors(
+ Effect.gen(function* () {
+ const user = yield* CurrentUser;
+ yield* getCapById(path.id);
+ yield* database.use((db) =>
+ db
+ .update(Db.videos)
+ .set({ public: payload.public })
+ .where(
+ and(
+ eq(Db.videos.id, path.id),
+ eq(Db.videos.ownerId, user.id),
+ ),
+ ),
+ );
+ const { cap } = yield* getCapById(path.id);
+ return cap;
+ }),
+ ),
+ )
+ .handle("updateCapTitle", ({ path, payload }) =>
+ withMappedErrors(
+ Effect.gen(function* () {
+ const user = yield* CurrentUser;
+ yield* getCapById(path.id);
+ const title = payload.title.trim();
+ if (!title) {
+ return yield* Effect.fail(new HttpApiError.BadRequest());
+ }
+
+ yield* database.use((db) =>
+ db
+ .update(Db.videos)
+ .set({ name: title })
+ .where(
+ and(
+ eq(Db.videos.id, path.id),
+ eq(Db.videos.ownerId, user.id),
+ ),
+ ),
+ );
+ yield* Effect.sync(() => {
+ revalidatePath("/dashboard/caps");
+ revalidatePath("/dashboard/shared-caps");
+ revalidatePath(`/s/${path.id}`);
+ });
+ const { cap } = yield* getCapById(path.id);
+ return cap;
+ }),
+ ),
+ )
+ .handle("updateCapPassword", ({ path, payload }) =>
+ withMappedErrors(
+ Effect.gen(function* () {
+ const user = yield* CurrentUser;
+ yield* getCapById(path.id);
+ const trimmedPassword = payload.password?.trim() ?? null;
+ const nextPassword = trimmedPassword
+ ? yield* Effect.tryPromise({
+ try: () => hashPassword(trimmedPassword),
+ catch: () => new HttpApiError.InternalServerError(),
+ })
+ : null;
+
+ yield* database.use((db) =>
+ db
+ .update(Db.videos)
+ .set({ password: nextPassword })
+ .where(
+ and(
+ eq(Db.videos.id, path.id),
+ eq(Db.videos.ownerId, user.id),
+ ),
+ ),
+ );
+ const { cap } = yield* getCapById(path.id);
+ return cap;
+ }),
+ ),
+ )
+ .handle("deleteCap", ({ path }) =>
+ withMappedErrors(
+ videos
+ .delete(path.id)
+ .pipe(Effect.map(() => ({ success: true as const }))),
+ ),
+ )
+ .handle("getPlayback", ({ path }) =>
+ withMappedErrors(getPlayback(path.id)),
+ )
+ .handle("getDownload", ({ path }) =>
+ withMappedErrors(
+ videos.getDownloadInfo(path.id).pipe(
+ Effect.flatMap(
+ Option.match({
+ onNone: () => Effect.fail(new HttpApiError.NotFound()),
+ onSome: (info) =>
+ Effect.succeed({
+ fileName: info.fileName,
+ url: info.downloadUrl,
+ }),
+ }),
+ ),
+ ),
+ ),
+ )
+ .handle("createComment", ({ path, payload }) =>
+ withMappedErrors(
+ createMobileComment({
+ videoId: path.id,
+ content: payload.content,
+ timestamp: payload.timestamp,
+ parentCommentId: payload.parentCommentId ?? null,
+ type: "text",
+ }),
+ ),
+ )
+ .handle("deleteComment", ({ path }) =>
+ withMappedErrors(
+ Effect.gen(function* () {
+ const user = yield* CurrentUser;
+ const result = yield* database.use((db) =>
+ db
+ .delete(Db.comments)
+ .where(
+ and(
+ eq(Db.comments.id, path.id),
+ eq(Db.comments.authorId, user.id),
+ ),
+ ),
+ );
+ const affectedRows = Array.isArray(result)
+ ? (result[0]?.affectedRows ?? 0)
+ : 0;
+ if (affectedRows === 0) {
+ return yield* Effect.fail(new HttpApiError.NotFound());
+ }
+ return { success: true as const };
+ }),
+ ),
+ )
+ .handle("createReaction", ({ path, payload }) =>
+ withMappedErrors(
+ createMobileComment({
+ videoId: path.id,
+ content: payload.content,
+ timestamp: payload.timestamp,
+ parentCommentId: null,
+ type: "emoji",
+ }),
+ ),
+ )
+ .handle("createUpload", ({ payload }) =>
+ withMappedErrors(createUpload(payload)),
+ )
+ .handle("updateUploadProgress", ({ path, payload }) =>
+ withMappedErrors(
+ videos
+ .updateUploadProgress({
+ videoId: path.id,
+ uploaded: Math.max(0, Math.trunc(payload.uploaded)),
+ total: Math.max(0, Math.trunc(payload.total)),
+ updatedAt: new Date(),
+ })
+ .pipe(Effect.map(() => ({ success: true as const }))),
+ ),
+ )
+ .handle("completeUpload", ({ path, payload }) =>
+ withMappedErrors(
+ Effect.gen(function* () {
+ const user = yield* CurrentUser;
+ const [video] = yield* database.use((db) =>
+ db
+ .select({
+ id: Db.videos.id,
+ ownerId: Db.videos.ownerId,
+ bucketId: Db.videos.bucket,
+ })
+ .from(Db.videos)
+ .where(
+ and(
+ eq(Db.videos.id, path.id),
+ eq(Db.videos.ownerId, user.id),
+ ),
+ ),
+ );
+ if (!video)
+ return yield* Effect.fail(new HttpApiError.NotFound());
+
+ const prefix = `${user.id}/${path.id}/`;
+ if (!payload.rawFileKey.startsWith(prefix)) {
+ return yield* Effect.fail(new HttpApiError.BadRequest());
+ }
+
+ if (payload.contentLength !== undefined) {
+ yield* database.use((db) =>
+ db
+ .update(Db.videoUploads)
+ .set({
+ uploaded: payload.contentLength,
+ total: payload.contentLength,
+ updatedAt: new Date(),
+ })
+ .where(eq(Db.videoUploads.videoId, path.id)),
+ );
+ }
+
+ yield* Effect.tryPromise(() =>
+ startVideoProcessingWorkflow({
+ videoId: path.id,
+ userId: user.id,
+ rawFileKey: payload.rawFileKey,
+ bucketId: video.bucketId,
+ processingMessage: "Starting video processing...",
+ startFailureMessage:
+ "Video uploaded, but processing could not start.",
+ mode: "singlepart",
+ }),
+ ).pipe(
+ Effect.catchAll((error) =>
+ Effect.logError(error).pipe(
+ Effect.flatMap(() =>
+ Effect.fail(new HttpApiError.InternalServerError()),
+ ),
+ ),
+ ),
+ );
+
+ return { success: true as const };
+ }),
+ ),
+ );
+ }),
+ ),
+ ),
+);
+
+const handler = apiToHandler(ApiLive);
+
+export const GET = handler;
+export const POST = handler;
+export const PATCH = handler;
+export const DELETE = handler;
diff --git a/apps/web/public/.well-known/workflow/v1/manifest.json b/apps/web/public/.well-known/workflow/v1/manifest.json
index 9548c938429..fda9d32654f 100644
--- a/apps/web/public/.well-known/workflow/v1/manifest.json
+++ b/apps/web/public/.well-known/workflow/v1/manifest.json
@@ -1,289 +1,292 @@
{
- "version": "1.0.0",
- "steps": {
- "node_modules/.pnpm/workflow@4.2.0-beta.73_@nestjs+common@11.1.17_reflect-metadata@0.2.2_rxjs@7.8.2__@nestj_2a6f07c63175c34d8d104665b34a32ad/node_modules/workflow/dist/internal/builtins.js": {
- "__builtin_response_array_buffer": {
- "stepId": "__builtin_response_array_buffer"
- },
- "__builtin_response_json": {
- "stepId": "__builtin_response_json"
- },
- "__builtin_response_text": {
- "stepId": "__builtin_response_text"
- }
- },
- "node_modules/.pnpm/workflow@4.2.0-beta.73_@nestjs+common@11.1.17_reflect-metadata@0.2.2_rxjs@7.8.2__@nestj_2a6f07c63175c34d8d104665b34a32ad/node_modules/workflow/dist/stdlib.js": {
- "fetch": {
- "stepId": "step//workflow@4.2.0-beta.73//fetch"
- }
- },
- "workflows/process-video.ts": {
- "cleanupRawUpload": {
- "stepId": "step//./workflows/process-video//cleanupRawUpload"
- },
- "processVideoOnMediaServer": {
- "stepId": "step//./workflows/process-video//processVideoOnMediaServer"
- },
- "saveMetadataAndComplete": {
- "stepId": "step//./workflows/process-video//saveMetadataAndComplete"
- },
- "setProcessingError": {
- "stepId": "step//./workflows/process-video//setProcessingError"
- },
- "validateProcessingRequest": {
- "stepId": "step//./workflows/process-video//validateProcessingRequest"
- }
- },
- "workflows/edit-video.ts": {
- "clearEditProcessingState": {
- "stepId": "step//./workflows/edit-video//clearEditProcessingState"
- },
- "invalidateEditedVideoCache": {
- "stepId": "step//./workflows/edit-video//invalidateEditedVideoCache"
- },
- "queueTranscriptionRegeneration": {
- "stepId": "step//./workflows/edit-video//queueTranscriptionRegeneration"
- },
- "renderVideoEditOnMediaServer": {
- "stepId": "step//./workflows/edit-video//renderVideoEditOnMediaServer"
- },
- "saveEditResultAndComplete": {
- "stepId": "step//./workflows/edit-video//saveEditResultAndComplete"
- },
- "validateEditRequest": {
- "stepId": "step//./workflows/edit-video//validateEditRequest"
- }
- },
- "workflows/import-loom-video.ts": {
- "downloadLoomToS3": {
- "stepId": "step//./workflows/import-loom-video//downloadLoomToS3"
- },
- "processVideoOnMediaServer": {
- "stepId": "step//./workflows/import-loom-video//processVideoOnMediaServer"
- },
- "saveMetadataAndComplete": {
- "stepId": "step//./workflows/import-loom-video//saveMetadataAndComplete"
- },
- "setProcessingError": {
- "stepId": "step//./workflows/import-loom-video//setProcessingError"
- }
- },
- "workflows/transcribe.ts": {
- "_enhanceAndSaveAudio": {
- "stepId": "step//./workflows/transcribe//_enhanceAndSaveAudio"
- },
- "_markEnhancedAudioProcessing": {
- "stepId": "step//./workflows/transcribe//_markEnhancedAudioProcessing"
- },
- "cleanupTempAudio": {
- "stepId": "step//./workflows/transcribe//cleanupTempAudio"
- },
- "extractAudio": {
- "stepId": "step//./workflows/transcribe//extractAudio"
- },
- "markNoAudio": {
- "stepId": "step//./workflows/transcribe//markNoAudio"
- },
- "markSkipped": {
- "stepId": "step//./workflows/transcribe//markSkipped"
- },
- "queueAiGeneration": {
- "stepId": "step//./workflows/transcribe//queueAiGeneration"
- },
- "saveTranscription": {
- "stepId": "step//./workflows/transcribe//saveTranscription"
- },
- "transcribeWithDeepgram": {
- "stepId": "step//./workflows/transcribe//transcribeWithDeepgram"
- },
- "validateVideo": {
- "stepId": "step//./workflows/transcribe//validateVideo"
- }
- },
- "workflows/generate-ai.ts": {
- "fetchTranscript": {
- "stepId": "step//./workflows/generate-ai//fetchTranscript"
- },
- "generateWithAi": {
- "stepId": "step//./workflows/generate-ai//generateWithAi"
- },
- "markSkipped": {
- "stepId": "step//./workflows/generate-ai//markSkipped"
- },
- "saveResults": {
- "stepId": "step//./workflows/generate-ai//saveResults"
- },
- "validateAndSetProcessing": {
- "stepId": "step//./workflows/generate-ai//validateAndSetProcessing"
- }
- }
- },
- "workflows": {
- "workflows/process-video.ts": {
- "processVideoWorkflow": {
- "workflowId": "workflow//./workflows/process-video//processVideoWorkflow",
- "graph": {
- "nodes": [
- {
- "id": "start",
- "type": "workflowStart",
- "data": {
- "label": "Start: processVideoWorkflow",
- "nodeKind": "workflow_start"
- }
- },
- {
- "id": "end",
- "type": "workflowEnd",
- "data": {
- "label": "Return",
- "nodeKind": "workflow_end"
- }
- }
- ],
- "edges": [
- {
- "id": "e_start_end",
- "source": "start",
- "target": "end",
- "type": "default"
- }
- ]
- }
- }
- },
- "workflows/edit-video.ts": {
- "editVideoWorkflow": {
- "workflowId": "workflow//./workflows/edit-video//editVideoWorkflow",
- "graph": {
- "nodes": [
- {
- "id": "start",
- "type": "workflowStart",
- "data": {
- "label": "Start: editVideoWorkflow",
- "nodeKind": "workflow_start"
- }
- },
- {
- "id": "end",
- "type": "workflowEnd",
- "data": {
- "label": "Return",
- "nodeKind": "workflow_end"
- }
- }
- ],
- "edges": [
- {
- "id": "e_start_end",
- "source": "start",
- "target": "end",
- "type": "default"
- }
- ]
- }
- }
- },
- "workflows/import-loom-video.ts": {
- "importLoomVideoWorkflow": {
- "workflowId": "workflow//./workflows/import-loom-video//importLoomVideoWorkflow",
- "graph": {
- "nodes": [
- {
- "id": "start",
- "type": "workflowStart",
- "data": {
- "label": "Start: importLoomVideoWorkflow",
- "nodeKind": "workflow_start"
- }
- },
- {
- "id": "end",
- "type": "workflowEnd",
- "data": {
- "label": "Return",
- "nodeKind": "workflow_end"
- }
- }
- ],
- "edges": [
- {
- "id": "e_start_end",
- "source": "start",
- "target": "end",
- "type": "default"
- }
- ]
- }
- }
- },
- "workflows/transcribe.ts": {
- "transcribeVideoWorkflow": {
- "workflowId": "workflow//./workflows/transcribe//transcribeVideoWorkflow",
- "graph": {
- "nodes": [
- {
- "id": "start",
- "type": "workflowStart",
- "data": {
- "label": "Start: transcribeVideoWorkflow",
- "nodeKind": "workflow_start"
- }
- },
- {
- "id": "end",
- "type": "workflowEnd",
- "data": {
- "label": "Return",
- "nodeKind": "workflow_end"
- }
- }
- ],
- "edges": [
- {
- "id": "e_start_end",
- "source": "start",
- "target": "end",
- "type": "default"
- }
- ]
- }
- }
- },
- "workflows/generate-ai.ts": {
- "generateAiWorkflow": {
- "workflowId": "workflow//./workflows/generate-ai//generateAiWorkflow",
- "graph": {
- "nodes": [
- {
- "id": "start",
- "type": "workflowStart",
- "data": {
- "label": "Start: generateAiWorkflow",
- "nodeKind": "workflow_start"
- }
- },
- {
- "id": "end",
- "type": "workflowEnd",
- "data": {
- "label": "Return",
- "nodeKind": "workflow_end"
- }
- }
- ],
- "edges": [
- {
- "id": "e_start_end",
- "source": "start",
- "target": "end",
- "type": "default"
- }
- ]
- }
- }
- }
- },
- "classes": {}
-}
+ "version": "1.0.0",
+ "steps": {
+ "workflows/generate-ai.ts": {
+ "fetchTranscript": {
+ "stepId": "step//./workflows/generate-ai//fetchTranscript"
+ },
+ "generateWithAi": {
+ "stepId": "step//./workflows/generate-ai//generateWithAi"
+ },
+ "markSkipped": {
+ "stepId": "step//./workflows/generate-ai//markSkipped"
+ },
+ "saveResults": {
+ "stepId": "step//./workflows/generate-ai//saveResults"
+ },
+ "validateAndSetProcessing": {
+ "stepId": "step//./workflows/generate-ai//validateAndSetProcessing"
+ }
+ },
+ "workflows/process-video.ts": {
+ "cleanupRawUpload": {
+ "stepId": "step//./workflows/process-video//cleanupRawUpload"
+ },
+ "processVideoOnMediaServer": {
+ "stepId": "step//./workflows/process-video//processVideoOnMediaServer"
+ },
+ "saveMetadataAndComplete": {
+ "stepId": "step//./workflows/process-video//saveMetadataAndComplete"
+ },
+ "setProcessingError": {
+ "stepId": "step//./workflows/process-video//setProcessingError"
+ },
+ "validateProcessingRequest": {
+ "stepId": "step//./workflows/process-video//validateProcessingRequest"
+ }
+ },
+ "node_modules/.pnpm/workflow@4.2.0-beta.73_@nestjs+common@11.1.17_reflect-metadata@0.2.2_rxjs@7.8.2__@nestj_a4704d8c92ead1c54153943c93b08dfc/node_modules/workflow/dist/stdlib.js": {
+ "fetch": {
+ "stepId": "step//workflow@4.2.0-beta.73//fetch"
+ }
+ },
+ "workflows/import-loom-video.ts": {
+ "downloadLoomToS3": {
+ "stepId": "step//./workflows/import-loom-video//downloadLoomToS3"
+ },
+ "processVideoOnMediaServer": {
+ "stepId": "step//./workflows/import-loom-video//processVideoOnMediaServer"
+ },
+ "saveMetadataAndComplete": {
+ "stepId": "step//./workflows/import-loom-video//saveMetadataAndComplete"
+ },
+ "setProcessingError": {
+ "stepId": "step//./workflows/import-loom-video//setProcessingError"
+ }
+ },
+ "node_modules/.pnpm/workflow@4.2.0-beta.73_@nestjs+common@11.1.17_reflect-metadata@0.2.2_rxjs@7.8.2__@nestj_a4704d8c92ead1c54153943c93b08dfc/node_modules/workflow/dist/internal/builtins.js": {
+ "__builtin_response_array_buffer": {
+ "stepId": "__builtin_response_array_buffer"
+ },
+ "__builtin_response_json": {
+ "stepId": "__builtin_response_json"
+ },
+ "__builtin_response_text": {
+ "stepId": "__builtin_response_text"
+ }
+ },
+ "workflows/edit-video.ts": {
+ "clearEditProcessingState": {
+ "stepId": "step//./workflows/edit-video//clearEditProcessingState"
+ },
+ "invalidateEditedVideoCache": {
+ "stepId": "step//./workflows/edit-video//invalidateEditedVideoCache"
+ },
+ "queueTranscriptionRegeneration": {
+ "stepId": "step//./workflows/edit-video//queueTranscriptionRegeneration"
+ },
+ "renderVideoEditOnMediaServer": {
+ "stepId": "step//./workflows/edit-video//renderVideoEditOnMediaServer"
+ },
+ "saveEditResultAndComplete": {
+ "stepId": "step//./workflows/edit-video//saveEditResultAndComplete"
+ },
+ "validateEditRequest": {
+ "stepId": "step//./workflows/edit-video//validateEditRequest"
+ }
+ },
+ "workflows/transcribe.ts": {
+ "_enhanceAndSaveAudio": {
+ "stepId": "step//./workflows/transcribe//_enhanceAndSaveAudio"
+ },
+ "_markEnhancedAudioProcessing": {
+ "stepId": "step//./workflows/transcribe//_markEnhancedAudioProcessing"
+ },
+ "cleanupTempAudio": {
+ "stepId": "step//./workflows/transcribe//cleanupTempAudio"
+ },
+ "extractAudio": {
+ "stepId": "step//./workflows/transcribe//extractAudio"
+ },
+ "markError": {
+ "stepId": "step//./workflows/transcribe//markError"
+ },
+ "markNoAudio": {
+ "stepId": "step//./workflows/transcribe//markNoAudio"
+ },
+ "markSkipped": {
+ "stepId": "step//./workflows/transcribe//markSkipped"
+ },
+ "queueAiGeneration": {
+ "stepId": "step//./workflows/transcribe//queueAiGeneration"
+ },
+ "saveTranscription": {
+ "stepId": "step//./workflows/transcribe//saveTranscription"
+ },
+ "transcribeWithDeepgram": {
+ "stepId": "step//./workflows/transcribe//transcribeWithDeepgram"
+ },
+ "validateVideo": {
+ "stepId": "step//./workflows/transcribe//validateVideo"
+ }
+ }
+ },
+ "workflows": {
+ "workflows/generate-ai.ts": {
+ "generateAiWorkflow": {
+ "workflowId": "workflow//./workflows/generate-ai//generateAiWorkflow",
+ "graph": {
+ "nodes": [
+ {
+ "id": "start",
+ "type": "workflowStart",
+ "data": {
+ "label": "Start: generateAiWorkflow",
+ "nodeKind": "workflow_start"
+ }
+ },
+ {
+ "id": "end",
+ "type": "workflowEnd",
+ "data": {
+ "label": "Return",
+ "nodeKind": "workflow_end"
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "e_start_end",
+ "source": "start",
+ "target": "end",
+ "type": "default"
+ }
+ ]
+ }
+ }
+ },
+ "workflows/process-video.ts": {
+ "processVideoWorkflow": {
+ "workflowId": "workflow//./workflows/process-video//processVideoWorkflow",
+ "graph": {
+ "nodes": [
+ {
+ "id": "start",
+ "type": "workflowStart",
+ "data": {
+ "label": "Start: processVideoWorkflow",
+ "nodeKind": "workflow_start"
+ }
+ },
+ {
+ "id": "end",
+ "type": "workflowEnd",
+ "data": {
+ "label": "Return",
+ "nodeKind": "workflow_end"
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "e_start_end",
+ "source": "start",
+ "target": "end",
+ "type": "default"
+ }
+ ]
+ }
+ }
+ },
+ "workflows/import-loom-video.ts": {
+ "importLoomVideoWorkflow": {
+ "workflowId": "workflow//./workflows/import-loom-video//importLoomVideoWorkflow",
+ "graph": {
+ "nodes": [
+ {
+ "id": "start",
+ "type": "workflowStart",
+ "data": {
+ "label": "Start: importLoomVideoWorkflow",
+ "nodeKind": "workflow_start"
+ }
+ },
+ {
+ "id": "end",
+ "type": "workflowEnd",
+ "data": {
+ "label": "Return",
+ "nodeKind": "workflow_end"
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "e_start_end",
+ "source": "start",
+ "target": "end",
+ "type": "default"
+ }
+ ]
+ }
+ }
+ },
+ "workflows/edit-video.ts": {
+ "editVideoWorkflow": {
+ "workflowId": "workflow//./workflows/edit-video//editVideoWorkflow",
+ "graph": {
+ "nodes": [
+ {
+ "id": "start",
+ "type": "workflowStart",
+ "data": {
+ "label": "Start: editVideoWorkflow",
+ "nodeKind": "workflow_start"
+ }
+ },
+ {
+ "id": "end",
+ "type": "workflowEnd",
+ "data": {
+ "label": "Return",
+ "nodeKind": "workflow_end"
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "e_start_end",
+ "source": "start",
+ "target": "end",
+ "type": "default"
+ }
+ ]
+ }
+ }
+ },
+ "workflows/transcribe.ts": {
+ "transcribeVideoWorkflow": {
+ "workflowId": "workflow//./workflows/transcribe//transcribeVideoWorkflow",
+ "graph": {
+ "nodes": [
+ {
+ "id": "start",
+ "type": "workflowStart",
+ "data": {
+ "label": "Start: transcribeVideoWorkflow",
+ "nodeKind": "workflow_start"
+ }
+ },
+ {
+ "id": "end",
+ "type": "workflowEnd",
+ "data": {
+ "label": "Return",
+ "nodeKind": "workflow_end"
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "e_start_end",
+ "source": "start",
+ "target": "end",
+ "type": "default"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "classes": {}
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 8ae22dd6d83..9af939bb312 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"dev": "pnpm run docker:up && trap 'pnpm run docker:stop' EXIT && dotenv -e .env -- turbo run dev --env-mode=loose --ui tui",
"dev:desktop": "pnpm run --filter=@cap/desktop dev",
"dev:manual": "pnpm run docker:up && trap 'pnpm run docker:stop' EXIT && dotenv -e .env -- turbo run dev --filter=!@cap/storybook --no-cache --concurrency 1",
+ "dev:mobile": "pnpm run docker:up && trap 'pnpm run docker:stop' EXIT && CAP_MOBILE_DISABLE_ASSOCIATED_DOMAINS=1 EXPO_PUBLIC_CAP_WEB_URL=${EXPO_PUBLIC_CAP_WEB_URL:-http://localhost:3000} dotenv -e .env -- turbo run dev --filter=@cap/web --filter=@cap/mobile --env-mode=loose --ui tui",
"dev:web": "pnpm dev --filter=!@cap/desktop",
"dev:windows": "start /b cmd /c \"pnpm run docker:up > nul\" && timeout /t 5 /nobreak > nul && dotenv -e .env -- turbo run dev --env-mode=loose --ui tui",
"docker:clean": "turbo run docker:clean",
@@ -59,6 +60,9 @@
"typescript": "^5.8.3"
},
"pnpm": {
+ "overrides": {
+ "react-native-worklets": "0.7.4"
+ },
"peerDependencyRules": {
"allowedVersions": {
"next-auth>next": ">=16.0.0"
diff --git a/packages/database/package.json b/packages/database/package.json
index a221a469a38..aa4bc320f88 100644
--- a/packages/database/package.json
+++ b/packages/database/package.json
@@ -58,6 +58,7 @@
"exports": {
".": "./index.ts",
"./auth/auth-options": "./auth/auth-options.ts",
+ "./auth/domain-utils": "./auth/domain-utils.ts",
"./auth/session": "./auth/session.ts",
"./schema": "./schema.ts",
"./crypto": "./crypto.ts",
@@ -71,6 +72,7 @@
"exports": {
".": "./dist/index.js",
"./auth/auth-options": "./dist/auth/auth-options.js",
+ "./auth/domain-utils": "./dist/auth/domain-utils.js",
"./auth/session": "./dist/auth/session.js",
"./schema": "./dist/schema.js",
"./crypto": "./dist/crypto.js",
diff --git a/packages/web-domain/src/Mobile.ts b/packages/web-domain/src/Mobile.ts
new file mode 100644
index 00000000000..3dfafc652c2
--- /dev/null
+++ b/packages/web-domain/src/Mobile.ts
@@ -0,0 +1,435 @@
+import {
+ HttpApi,
+ HttpApiEndpoint,
+ HttpApiError,
+ HttpApiGroup,
+ OpenApi,
+} from "@effect/platform";
+import { Schema } from "effect";
+import { HttpAuthMiddleware } from "./Authentication.ts";
+import { CommentId } from "./Comment.ts";
+import { FolderColor, FolderId } from "./Folder.ts";
+import { OrganisationId } from "./Organisation.ts";
+import { UploadTarget } from "./Storage.ts";
+import { UserId } from "./User.ts";
+import { UploadPhase, VideoId } from "./Video.ts";
+
+export const MobileApiKeyResponse = Schema.Struct({
+ type: Schema.Literal("api_key"),
+ apiKey: Schema.String,
+ userId: UserId,
+});
+
+export const MobileSuccessResponse = Schema.Struct({
+ success: Schema.Literal(true),
+});
+
+export const MobileAuthConfigResponse = Schema.Struct({
+ googleAuthAvailable: Schema.Boolean,
+ workosAuthAvailable: Schema.Boolean,
+});
+
+export const MobileSessionRequestParams = Schema.Struct({
+ redirectUri: Schema.optional(Schema.String),
+ provider: Schema.optional(Schema.Literal("google", "workos")),
+ organizationId: Schema.optional(Schema.String),
+});
+
+export const MobileEmailSessionRequestInput = Schema.Struct({
+ email: Schema.String,
+});
+
+export const MobileEmailSessionVerifyInput = Schema.Struct({
+ email: Schema.String,
+ code: Schema.String,
+});
+
+export const MobileAuthHeaders = Schema.Struct({
+ authorization: Schema.optional(Schema.String),
+});
+
+export const MobileUser = Schema.Struct({
+ id: UserId,
+ name: Schema.NullOr(Schema.String),
+ email: Schema.String,
+ imageUrl: Schema.NullOr(Schema.String),
+ activeOrganizationId: OrganisationId,
+});
+
+export const MobileOrganization = Schema.Struct({
+ id: OrganisationId,
+ name: Schema.String,
+ iconUrl: Schema.NullOr(Schema.String),
+ role: Schema.Literal("owner", "admin", "member"),
+});
+
+export const MobileFolder = Schema.Struct({
+ id: FolderId,
+ name: Schema.String,
+ color: FolderColor,
+ parentId: Schema.NullOr(FolderId),
+ videoCount: Schema.Number,
+});
+
+export const MobileUploadProgress = Schema.Struct({
+ uploaded: Schema.Number,
+ total: Schema.Number,
+ phase: UploadPhase,
+ processingProgress: Schema.Number,
+ processingMessage: Schema.NullOr(Schema.String),
+ processingError: Schema.NullOr(Schema.String),
+});
+
+export const MobileCapSummary = Schema.Struct({
+ id: VideoId,
+ shareUrl: Schema.String,
+ title: Schema.String,
+ createdAt: Schema.String,
+ updatedAt: Schema.String,
+ ownerName: Schema.String,
+ durationSeconds: Schema.NullOr(Schema.Number),
+ thumbnailUrl: Schema.NullOr(Schema.String),
+ folderId: Schema.NullOr(FolderId),
+ public: Schema.Boolean,
+ protected: Schema.Boolean,
+ viewCount: Schema.Number,
+ commentCount: Schema.Number,
+ reactionCount: Schema.Number,
+ upload: Schema.NullOr(MobileUploadProgress),
+});
+
+export const MobileComment = Schema.Struct({
+ id: CommentId,
+ videoId: VideoId,
+ type: Schema.Literal("text", "emoji"),
+ content: Schema.String,
+ timestamp: Schema.NullOr(Schema.Number),
+ parentCommentId: Schema.NullOr(CommentId),
+ createdAt: Schema.String,
+ updatedAt: Schema.String,
+ author: Schema.Struct({
+ id: UserId,
+ name: Schema.NullOr(Schema.String),
+ imageUrl: Schema.NullOr(Schema.String),
+ }),
+});
+
+export const MobileChapter = Schema.Struct({
+ title: Schema.String,
+ start: Schema.Number,
+});
+
+export const MobileCapDetail = Schema.Struct({
+ cap: MobileCapSummary,
+ summary: Schema.NullOr(Schema.String),
+ chapters: Schema.Array(MobileChapter),
+ transcriptionStatus: Schema.NullOr(
+ Schema.Literal("PROCESSING", "COMPLETE", "ERROR", "SKIPPED", "NO_AUDIO"),
+ ),
+ comments: Schema.Array(MobileComment),
+ shareUrl: Schema.String,
+});
+
+export const MobileCapsListParams = Schema.Struct({
+ folderId: Schema.optional(Schema.String),
+ page: Schema.optional(Schema.String),
+ limit: Schema.optional(Schema.String),
+});
+
+export const MobileCapsListResponse = Schema.Struct({
+ folders: Schema.Array(MobileFolder),
+ caps: Schema.Array(MobileCapSummary),
+ page: Schema.Number,
+ limit: Schema.Number,
+ total: Schema.Number,
+ hasMore: Schema.Boolean,
+});
+
+export const MobileBootstrapResponse = Schema.Struct({
+ user: MobileUser,
+ organizations: Schema.Array(MobileOrganization),
+ activeOrganizationId: Schema.NullOr(OrganisationId),
+ rootFolders: Schema.Array(MobileFolder),
+});
+
+export const MobileActiveOrganizationInput = Schema.Struct({
+ organizationId: OrganisationId,
+});
+
+export const MobileCapSharingInput = Schema.Struct({
+ public: Schema.Boolean,
+});
+
+export const MobileCapTitleInput = Schema.Struct({
+ title: Schema.String,
+});
+
+export const MobileCapPasswordInput = Schema.Struct({
+ password: Schema.NullOr(Schema.String),
+});
+
+export const MobileFolderCreateInput = Schema.Struct({
+ name: Schema.String,
+ color: Schema.optional(FolderColor),
+});
+
+export const MobileVideoPath = Schema.Struct({
+ id: VideoId,
+});
+
+export const MobileCommentPath = Schema.Struct({
+ id: CommentId,
+});
+
+export const MobileUploadPath = Schema.Struct({
+ id: VideoId,
+});
+
+export const MobileCommentCreateInput = Schema.Struct({
+ content: Schema.String,
+ timestamp: Schema.NullOr(Schema.Number),
+ parentCommentId: Schema.optional(Schema.NullOr(CommentId)),
+});
+
+export const MobileReactionCreateInput = Schema.Struct({
+ content: Schema.String,
+ timestamp: Schema.NullOr(Schema.Number),
+});
+
+export const MobilePlaybackResponse = Schema.Struct({
+ kind: Schema.Literal("mp4", "hls"),
+ url: Schema.String,
+ transcriptUrl: Schema.NullOr(Schema.String),
+});
+
+export const MobileDownloadResponse = Schema.Struct({
+ fileName: Schema.String,
+ url: Schema.String,
+});
+
+export const MobileUploadCreateInput = Schema.Struct({
+ organizationId: Schema.optional(OrganisationId),
+ folderId: Schema.optional(FolderId),
+ fileName: Schema.String,
+ contentType: Schema.String,
+ contentLength: Schema.optional(Schema.Number),
+ durationSeconds: Schema.optional(Schema.Number),
+ width: Schema.optional(Schema.Number),
+ height: Schema.optional(Schema.Number),
+ fps: Schema.optional(Schema.Number),
+});
+
+export const MobileUploadCreateResponse = Schema.Struct({
+ id: VideoId,
+ shareUrl: Schema.String,
+ rawFileKey: Schema.String,
+ upload: UploadTarget,
+ cap: MobileCapSummary,
+});
+
+export const MobileUploadProgressInput = Schema.Struct({
+ uploaded: Schema.Number,
+ total: Schema.Number,
+});
+
+export const MobileUploadCompleteInput = Schema.Struct({
+ rawFileKey: Schema.String,
+ contentLength: Schema.optional(Schema.Number),
+});
+
+export class MobileHttpApi extends HttpApiGroup.make("mobile")
+ .add(
+ HttpApiEndpoint.get("getAuthConfig", "/session/config").addSuccess(
+ MobileAuthConfigResponse,
+ ),
+ )
+ .add(
+ HttpApiEndpoint.get("requestSession", "/session/request")
+ .setUrlParams(MobileSessionRequestParams)
+ .addSuccess(MobileApiKeyResponse)
+ .addError(HttpApiError.InternalServerError)
+ .addError(HttpApiError.BadRequest)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("requestEmailSession", "/session/email/request")
+ .setPayload(MobileEmailSessionRequestInput)
+ .addSuccess(MobileSuccessResponse)
+ .addError(HttpApiError.InternalServerError)
+ .addError(HttpApiError.BadRequest)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("verifyEmailSession", "/session/email/verify")
+ .setPayload(MobileEmailSessionVerifyInput)
+ .addSuccess(MobileApiKeyResponse)
+ .addError(HttpApiError.InternalServerError)
+ .addError(HttpApiError.BadRequest)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("revokeSession", "/session/revoke")
+ .setHeaders(MobileAuthHeaders)
+ .addSuccess(MobileSuccessResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.get("bootstrap", "/bootstrap")
+ .addSuccess(MobileBootstrapResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.patch("setActiveOrganization", "/user/active-organization")
+ .setPayload(MobileActiveOrganizationInput)
+ .addSuccess(MobileBootstrapResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.get("listCaps", "/caps")
+ .setUrlParams(MobileCapsListParams)
+ .addSuccess(MobileCapsListResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("createFolder", "/folders")
+ .setPayload(MobileFolderCreateInput)
+ .addSuccess(MobileFolder)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.BadRequest)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.get("getCap", "/caps/:id")
+ .setPath(MobileVideoPath)
+ .addSuccess(MobileCapDetail)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.patch("updateCapSharing", "/caps/:id/sharing")
+ .setPath(MobileVideoPath)
+ .setPayload(MobileCapSharingInput)
+ .addSuccess(MobileCapSummary)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.patch("updateCapTitle", "/caps/:id/title")
+ .setPath(MobileVideoPath)
+ .setPayload(MobileCapTitleInput)
+ .addSuccess(MobileCapSummary)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.BadRequest)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.patch("updateCapPassword", "/caps/:id/password")
+ .setPath(MobileVideoPath)
+ .setPayload(MobileCapPasswordInput)
+ .addSuccess(MobileCapSummary)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.BadRequest)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.del("deleteCap", "/caps/:id")
+ .setPath(MobileVideoPath)
+ .addSuccess(MobileSuccessResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.get("getPlayback", "/caps/:id/playback")
+ .setPath(MobileVideoPath)
+ .addSuccess(MobilePlaybackResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.get("getDownload", "/caps/:id/download")
+ .setPath(MobileVideoPath)
+ .addSuccess(MobileDownloadResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("createComment", "/caps/:id/comments")
+ .setPath(MobileVideoPath)
+ .setPayload(MobileCommentCreateInput)
+ .addSuccess(MobileComment)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.del("deleteComment", "/comments/:id")
+ .setPath(MobileCommentPath)
+ .addSuccess(MobileSuccessResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("createReaction", "/caps/:id/reactions")
+ .setPath(MobileVideoPath)
+ .setPayload(MobileReactionCreateInput)
+ .addSuccess(MobileComment)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("createUpload", "/uploads")
+ .setPayload(MobileUploadCreateInput)
+ .addSuccess(MobileUploadCreateResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("updateUploadProgress", "/uploads/:id/progress")
+ .setPath(MobileUploadPath)
+ .setPayload(MobileUploadProgressInput)
+ .addSuccess(MobileSuccessResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ )
+ .add(
+ HttpApiEndpoint.post("completeUpload", "/uploads/:id/complete")
+ .setPath(MobileUploadPath)
+ .setPayload(MobileUploadCompleteInput)
+ .addSuccess(MobileSuccessResponse)
+ .middleware(HttpAuthMiddleware)
+ .addError(HttpApiError.Forbidden)
+ .addError(HttpApiError.NotFound),
+ ) {}
+
+export class MobileApiContract extends HttpApi.make("cap-mobile-api")
+ .add(MobileHttpApi)
+ .annotateContext(
+ OpenApi.annotations({
+ title: "Cap Mobile API",
+ description: "Authenticated API used by the Cap iOS app",
+ }),
+ )
+ .prefix("/api/mobile") {}
diff --git a/packages/web-domain/src/index.ts b/packages/web-domain/src/index.ts
index b8aca7049a4..dc3db577ddd 100644
--- a/packages/web-domain/src/index.ts
+++ b/packages/web-domain/src/index.ts
@@ -9,6 +9,7 @@ export * as ImageUpload from "./ImageUpload.ts";
export * as Language from "./Language.ts";
export * from "./Language.ts";
export * as Loom from "./Loom.ts";
+export * as Mobile from "./Mobile.ts";
export * as Organisation from "./Organisation.ts";
export * from "./Organisation.ts";
export * as Policy from "./Policy.ts";
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e1003cb70eb..cd167e2fdd3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+overrides:
+ react-native-worklets: 0.7.4
+
importers:
.:
@@ -115,7 +118,7 @@ importers:
version: 0.14.10(solid-js@1.9.6)
'@solidjs/start':
specifier: ^1.1.3
- version: 1.1.3(@testing-library/jest-dom@6.5.0)(@types/node@22.15.17)(jiti@2.6.1)(solid-js@1.9.6)(terser@5.44.0)(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)
+ version: 1.1.3(@testing-library/jest-dom@6.5.0)(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(solid-js@1.9.6)(terser@5.44.0)(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)
'@tanstack/solid-query':
specifier: ^5.51.21
version: 5.75.4(solid-js@1.9.6)
@@ -205,7 +208,7 @@ importers:
version: 9.0.1
vinxi:
specifier: ^0.5.6
- version: 0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1)
+ version: 0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1)
webcodecs:
specifier: ^0.1.0
version: 0.1.0
@@ -242,19 +245,19 @@ importers:
version: 5.8.3
vite:
specifier: ^6.3.5
- version: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ version: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
vite-plugin-top-level-await:
specifier: ^1.4.4
- version: 1.6.0(@swc/helpers@0.5.17)(rollup@4.40.2)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))
+ version: 1.6.0(@swc/helpers@0.5.17)(rollup@4.40.2)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
vite-plugin-wasm:
specifier: ^3.4.1
- version: 3.5.0(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))
+ version: 3.5.0(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
vite-tsconfig-paths:
specifier: ^5.0.1
- version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))
+ version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
vitest:
specifier: ~2.1.9
- version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0)
+ version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.44.0)
apps/discord-bot:
dependencies:
@@ -285,7 +288,7 @@ importers:
devDependencies:
'@cloudflare/vitest-pool-workers':
specifier: ^0.6.4
- version: 0.6.16(@cloudflare/workers-types@4.20250507.0)(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0))
+ version: 0.6.16(@cloudflare/workers-types@4.20250507.0)(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.44.0))
'@cloudflare/workers-types':
specifier: ^4.20250214.0
version: 4.20250507.0
@@ -294,7 +297,7 @@ importers:
version: 5.8.3
vitest:
specifier: ~2.1.9
- version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0)
+ version: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.44.0)
wrangler:
specifier: ^3.109.1
version: 3.114.8(@cloudflare/workers-types@4.20250507.0)
@@ -321,6 +324,133 @@ importers:
specifier: latest
version: 1.3.14
+ apps/mobile:
+ dependencies:
+ '@cap/web-domain':
+ specifier: workspace:*
+ version: link:../../packages/web-domain
+ '@expo/config-plugins':
+ specifier: ~55.0.9
+ version: 55.0.9
+ '@expo/metro-runtime':
+ specifier: ~55.0.11
+ version: 55.0.11(@expo/dom-webview@55.0.6)(expo@55.0.24)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@shopify/flash-list':
+ specifier: 2.0.2
+ version: 2.0.2(@babel/runtime@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ effect:
+ specifier: ^3.18.4
+ version: 3.18.4
+ expo:
+ specifier: ~55.0.24
+ version: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-clipboard:
+ specifier: ~55.0.13
+ version: 55.0.13(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-constants:
+ specifier: ~55.0.16
+ version: 55.0.16(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))
+ expo-dev-client:
+ specifier: ~55.0.34
+ version: 55.0.34(expo@55.0.24)
+ expo-document-picker:
+ specifier: ~55.0.13
+ version: 55.0.13(expo@55.0.24)
+ expo-file-system:
+ specifier: ~55.0.20
+ version: 55.0.20(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))
+ expo-font:
+ specifier: ~55.0.7
+ version: 55.0.7(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-glass-effect:
+ specifier: ~55.0.11
+ version: 55.0.11(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-image:
+ specifier: ~55.0.10
+ version: 55.0.10(expo@55.0.24)(react-native-web@0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-image-picker:
+ specifier: ~55.0.20
+ version: 55.0.20(expo@55.0.24)
+ expo-linking:
+ specifier: ~55.0.15
+ version: 55.0.15(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-media-library:
+ specifier: ~55.0.17
+ version: 55.0.17(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))
+ expo-modules-core:
+ specifier: ~55.0.25
+ version: 55.0.25(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-router:
+ specifier: ~55.0.14
+ version: 55.0.14(eed5efedde241c317111b390e3f7dd2b)
+ expo-secure-store:
+ specifier: ~55.0.14
+ version: 55.0.14(expo@55.0.24)
+ expo-sharing:
+ specifier: ~55.0.19
+ version: 55.0.19(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-symbols:
+ specifier: ~55.0.8
+ version: 55.0.8(expo-font@55.0.7)(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-video:
+ specifier: ~55.0.17
+ version: 55.0.17(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-web-browser:
+ specifier: ~55.0.16
+ version: 55.0.16(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))
+ react:
+ specifier: 19.2.0
+ version: 19.2.0
+ react-dom:
+ specifier: 19.2.0
+ version: 19.2.0(react@19.2.0)
+ react-native:
+ specifier: 0.83.6
+ version: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ react-native-gesture-handler:
+ specifier: ~2.30.0
+ version: 2.30.1(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-reanimated:
+ specifier: 4.2.1
+ version: 4.2.1(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-safe-area-context:
+ specifier: ~5.6.2
+ version: 5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-screens:
+ specifier: ~4.23.0
+ version: 4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-svg:
+ specifier: 15.15.5
+ version: 15.15.5(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-web:
+ specifier: ~0.21.0
+ version: 0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ react-native-worklets:
+ specifier: 0.7.4
+ version: 0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ devDependencies:
+ '@testing-library/react-native':
+ specifier: ^13.3.3
+ version: 13.3.3(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react-test-renderer@19.2.0(react@19.2.0))(react@19.2.0)
+ '@types/react':
+ specifier: 19.2.14
+ version: 19.2.14
+ '@types/react-test-renderer':
+ specifier: ^19.1.0
+ version: 19.1.0
+ babel-preset-expo:
+ specifier: ~55.0.21
+ version: 55.0.21(@babel/core@7.27.1)(@babel/runtime@7.27.1)(expo@55.0.24)(react-refresh@0.14.2)
+ react-test-renderer:
+ specifier: 19.2.0
+ version: 19.2.0(react@19.2.0)
+ typescript:
+ specifier: ~5.9.2
+ version: 5.9.3
+ vitest:
+ specifier: ^3.2.0
+ version: 3.2.4(@types/debug@4.1.12)(@types/node@22.15.17)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
+
apps/storybook:
dependencies:
'@cap/ui-solid':
@@ -365,16 +495,16 @@ importers:
version: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.7.4)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.7.4))
storybook-solidjs-vite:
specifier: ^1.0.0-beta.2
- version: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.7.4)))(esbuild@0.25.5)(rollup@4.40.2)(solid-js@1.9.6)(storybook@8.6.12(prettier@3.7.4))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))
+ version: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.7.4)))(esbuild@0.25.5)(rollup@4.40.2)(solid-js@1.9.6)(storybook@8.6.12(prettier@3.7.4))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))
typescript:
specifier: ^5.8.3
version: 5.8.3
vite:
specifier: ^6.3.5
- version: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ version: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
vite-plugin-solid:
specifier: ^2.10.2
- version: 2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))
+ version: 2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
apps/web:
dependencies:
@@ -581,7 +711,7 @@ importers:
version: 2.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@workos-inc/node':
specifier: ^7.34.0
- version: 7.50.0(express@5.1.0)(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
+ version: 7.50.0(express@5.1.0)(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
aws-sdk:
specifier: ^2.1530.0
version: 2.1692.0
@@ -629,7 +759,7 @@ importers:
version: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
geist:
specifier: ^1.3.1
- version: 1.4.2(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
+ version: 1.4.2(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
gif.js:
specifier: 0.2.0
version: 0.2.0
@@ -668,10 +798,10 @@ importers:
version: 12.20.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
next:
specifier: 16.2.1
- version: 16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
next-auth:
specifier: ^4.24.5
- version: 4.24.11(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@6.10.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 4.24.11(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@6.10.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
next-mdx-remote:
specifier: ^6.0.0
version: 6.0.0(@types/react@19.2.14)(acorn@8.15.0)(react@19.2.4)
@@ -716,7 +846,7 @@ importers:
version: 5.28.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
recharts:
specifier: ^3.3.0
- version: 3.4.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@17.0.2)(react@19.2.4)(redux@5.0.1)
+ version: 3.4.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@19.2.6)(react@19.2.4)(redux@5.0.1)
rehype-pretty-code:
specifier: ^0.14.1
version: 0.14.1(shiki@3.23.0)
@@ -761,7 +891,7 @@ importers:
version: 9.0.1
workflow:
specifier: 4.2.0-beta.73
- version: 4.2.0-beta.73(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@opentelemetry/api@1.9.0)(@swc/cli@0.8.0(@swc/core@1.15.3(@swc/helpers@0.5.17))(chokidar@5.0.0))(@swc/core@1.15.3(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(magicast@0.3.5)(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.8.3)
+ version: 4.2.0-beta.73(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@opentelemetry/api@1.9.0)(@swc/cli@0.8.0(@swc/core@1.15.3(@swc/helpers@0.5.17))(chokidar@5.0.0))(@swc/core@1.15.3(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(magicast@0.3.5)(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.8.3)
zod:
specifier: ^3.25.76
version: 3.25.76
@@ -831,7 +961,7 @@ importers:
version: 5.8.3
vitest:
specifier: ^3.2.0
- version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.43)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.44.0)(yaml@2.8.1)
+ version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.43)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
apps/web-cluster:
dependencies:
@@ -879,10 +1009,10 @@ importers:
version: 1.0.0-beta.42
tsdown:
specifier: ^0.15.6
- version: 0.15.6(typescript@5.8.3)
+ version: 0.15.6(typescript@5.9.3)
tsup:
specifier: ^8.5.0
- version: 8.5.0(@swc/core@1.15.5(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.3)(typescript@5.8.3)(yaml@2.8.1)
+ version: 8.5.0(@swc/core@1.15.5(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.3)(typescript@5.9.3)(yaml@2.8.1)
devDependencies:
concurrently:
specifier: ^9.2.1
@@ -895,13 +1025,13 @@ importers:
dependencies:
'@pulumi/github':
specifier: ^6.7.0
- version: 6.7.2(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))(typescript@5.8.3)
+ version: 6.7.2(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))(typescript@5.9.3)
'@pulumi/pulumi':
specifier: ^3.201.0
- version: 3.201.0(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))(typescript@5.8.3)
+ version: 3.201.0(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))(typescript@5.9.3)
'@pulumiverse/vercel':
specifier: ^1.14.3
- version: 1.14.3(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))(typescript@5.8.3)
+ version: 1.14.3(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))(typescript@5.9.3)
zod:
specifier: ^3
version: 3.25.76
@@ -914,13 +1044,13 @@ importers:
dependencies:
'@vitejs/plugin-react':
specifier: ^4.0.3
- version: 4.4.1(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))
+ version: 4.4.1(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
vite:
specifier: ^6.3.5
- version: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ version: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
vite-tsconfig-paths:
specifier: ^4.2.0
- version: 4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))
+ version: 4.3.2(typescript@5.9.3)(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
zod:
specifier: ^3
version: 3.25.76
@@ -930,16 +1060,16 @@ importers:
version: 20.17.43
'@typescript-eslint/eslint-plugin':
specifier: ^5.59.6
- version: 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)
+ version: 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/parser':
specifier: ^5.59.6
- version: 5.62.0(eslint@8.57.1)(typescript@5.8.3)
+ version: 5.62.0(eslint@8.57.1)(typescript@5.9.3)
eslint:
specifier: ^8.41.0
version: 8.57.1
eslint-config-next:
specifier: 13.3.0
- version: 13.3.0(eslint@8.57.1)(typescript@5.8.3)
+ version: 13.3.0(eslint@8.57.1)(typescript@5.9.3)
eslint-config-prettier:
specifier: ^8.8.0
version: 8.10.0(eslint@8.57.1)
@@ -957,7 +1087,7 @@ importers:
version: 4.6.2(eslint@8.57.1)
eslint-plugin-tailwindcss:
specifier: ^3.12.0
- version: 3.18.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.8.3)))
+ version: 3.18.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.9.3)))
eslint-utils:
specifier: ^3.0.0
version: 3.0.0(eslint@8.57.1)
@@ -1011,13 +1141,13 @@ importers:
version: 5.1.5
next:
specifier: 15.5.9
- version: 15.5.9(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ version: 15.5.9(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
next-auth:
specifier: ^4.24.5
- version: 4.24.11(next@15.5.9(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ version: 4.24.11(next@15.5.9(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react-email:
specifier: ^4.0.16
- version: 4.0.16(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ version: 4.0.16(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
resend:
specifier: 4.6.0
version: 4.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@@ -1069,7 +1199,7 @@ importers:
dependencies:
'@t3-oss/env-nextjs':
specifier: ^0.12.0
- version: 0.12.0(typescript@5.8.3)(valibot@1.0.0-rc.1(typescript@5.8.3))(zod@3.25.76)
+ version: 0.12.0(typescript@5.9.3)(valibot@1.0.0-rc.1(typescript@5.9.3))(zod@3.25.76)
zod:
specifier: ^3.25.76
version: 3.25.76
@@ -1191,7 +1321,7 @@ importers:
version: 19.2.3(@types/react@19.2.14)
'@vitejs/plugin-react':
specifier: ^4.0.3
- version: 4.4.1(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))
+ version: 4.4.1(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
autoprefixer:
specifier: ^10.4.16
version: 10.4.21(postcss@8.5.3)
@@ -1233,10 +1363,10 @@ importers:
version: 5.8.3
vite:
specifier: ^6.3.5
- version: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ version: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
vite-tsconfig-paths:
specifier: ^4.2.1
- version: 4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))
+ version: 4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
packages/ui-solid:
dependencies:
@@ -1251,7 +1381,7 @@ importers:
version: 1.9.6
tailwindcss:
specifier: ^3.4.10
- version: 3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))
+ version: 3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))
zod:
specifier: ^3
version: 3.25.76
@@ -1264,10 +1394,10 @@ importers:
version: 2.2.336
'@kobalte/tailwindcss':
specifier: ^0.9.0
- version: 0.9.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3)))
+ version: 0.9.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3)))
'@tailwindcss/typography':
specifier: ^0.5.9
- version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3)))
+ version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3)))
autoprefixer:
specifier: ^10.4.20
version: 10.4.21(postcss@8.5.3)
@@ -1279,16 +1409,16 @@ importers:
version: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.7.4)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.7.4))
tailwind-scrollbar:
specifier: ^3.1.0
- version: 3.1.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3)))
+ version: 3.1.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3)))
tailwindcss-animate:
specifier: ^1.0.6
- version: 1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3)))
+ version: 1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3)))
unplugin-auto-import:
specifier: ^0.18.2
version: 0.18.6(rollup@4.40.2)
unplugin-fonts:
specifier: ^1.1.1
- version: 1.3.1(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))
+ version: 1.3.1(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
unplugin-icons:
specifier: ^0.19.2
version: 0.19.3
@@ -1406,7 +1536,7 @@ importers:
version: 3.18.4
next:
specifier: 15.5.9
- version: 15.5.9(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 15.5.9(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
server-only:
specifier: ^0.0.1
version: 0.0.1
@@ -1815,40 +1945,73 @@ packages:
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
+ '@babel/code-frame@7.29.0':
+ resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/compat-data@7.27.2':
resolution: {integrity: sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==}
engines: {node: '>=6.9.0'}
+ '@babel/compat-data@7.29.3':
+ resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==}
+ engines: {node: '>=6.9.0'}
+
'@babel/core@7.27.1':
resolution: {integrity: sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==}
engines: {node: '>=6.9.0'}
- '@babel/generator@7.27.1':
- resolution: {integrity: sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==}
+ '@babel/generator@7.28.3':
+ resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==}
engines: {node: '>=6.9.0'}
- '@babel/generator@7.27.5':
- resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==}
+ '@babel/generator@7.29.1':
+ resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
engines: {node: '>=6.9.0'}
- '@babel/generator@7.28.3':
- resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==}
+ '@babel/helper-annotate-as-pure@7.27.3':
+ resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==}
engines: {node: '>=6.9.0'}
'@babel/helper-compilation-targets@7.27.2':
resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-compilation-targets@7.28.6':
+ resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-create-class-features-plugin@7.29.3':
+ resolution: {integrity: sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-create-regexp-features-plugin@7.28.5':
+ resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-define-polyfill-provider@0.6.8':
+ resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
'@babel/helper-globals@7.28.0':
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-member-expression-to-functions@7.28.5':
+ resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-module-imports@7.18.6':
resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==}
engines: {node: '>=6.9.0'}
- '@babel/helper-module-imports@7.27.1':
- resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
+ '@babel/helper-module-imports@7.28.6':
+ resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-transforms@7.27.1':
@@ -1857,10 +2020,40 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0
+ '@babel/helper-module-transforms@7.28.6':
+ resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-optimise-call-expression@7.27.1':
+ resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-plugin-utils@7.27.1':
resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-plugin-utils@7.28.6':
+ resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-remap-async-to-generator@7.27.1':
+ resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-replace-supers@7.28.6':
+ resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.27.1':
+ resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
@@ -1869,10 +2062,18 @@ packages:
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-validator-option@7.27.1':
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
engines: {node: '>=6.9.0'}
+ '@babel/helper-wrap-function@7.28.6':
+ resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helpers@7.27.1':
resolution: {integrity: sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==}
engines: {node: '>=6.9.0'}
@@ -1896,18 +2097,335 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
+ '@babel/parser@7.29.3':
+ resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-proposal-decorators@7.29.0':
+ resolution: {integrity: sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-proposal-export-default-from@7.27.1':
+ resolution: {integrity: sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-async-generators@7.8.4':
+ resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-bigint@7.8.3':
+ resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-class-properties@7.12.13':
+ resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-class-static-block@7.14.5':
+ resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-decorators@7.28.6':
+ resolution: {integrity: sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-dynamic-import@7.8.3':
+ resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-export-default-from@7.28.6':
+ resolution: {integrity: sha512-Svlx1fjJFnNz0LZeUaybRukSxZI3KkpApUmIRzEdXC5k8ErTOz0OD0kNrICi5Vc3GlpP5ZCeRyRO+mfWTSz+iQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-flow@7.28.6':
+ resolution: {integrity: sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-import-attributes@7.28.6':
+ resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-import-meta@7.10.4':
+ resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-json-strings@7.8.3':
+ resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/plugin-syntax-jsx@7.27.1':
resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/plugin-syntax-jsx@7.28.6':
+ resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-logical-assignment-operators@7.10.4':
+ resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3':
+ resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-numeric-separator@7.10.4':
+ resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-object-rest-spread@7.8.3':
+ resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-optional-catch-binding@7.8.3':
+ resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-optional-chaining@7.8.3':
+ resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-private-property-in-object@7.14.5':
+ resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-top-level-await@7.14.5':
+ resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/plugin-syntax-typescript@7.27.1':
resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/plugin-syntax-typescript@7.28.6':
+ resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-arrow-functions@7.27.1':
+ resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-async-generator-functions@7.29.0':
+ resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-async-to-generator@7.28.6':
+ resolution: {integrity: sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-block-scoping@7.28.6':
+ resolution: {integrity: sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-class-properties@7.27.1':
+ resolution: {integrity: sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-class-properties@7.28.6':
+ resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-class-static-block@7.28.6':
+ resolution: {integrity: sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.12.0
+
+ '@babel/plugin-transform-classes@7.28.4':
+ resolution: {integrity: sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-classes@7.28.6':
+ resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-computed-properties@7.28.6':
+ resolution: {integrity: sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-destructuring@7.28.5':
+ resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-export-namespace-from@7.27.1':
+ resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-flow-strip-types@7.27.1':
+ resolution: {integrity: sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-for-of@7.27.1':
+ resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-function-name@7.27.1':
+ resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-literals@7.27.1':
+ resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-logical-assignment-operators@7.28.6':
+ resolution: {integrity: sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-modules-commonjs@7.28.6':
+ resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-named-capturing-groups-regex@7.29.0':
+ resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-transform-nullish-coalescing-operator@7.27.1':
+ resolution: {integrity: sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-nullish-coalescing-operator@7.28.6':
+ resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-numeric-separator@7.28.6':
+ resolution: {integrity: sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-object-rest-spread@7.28.6':
+ resolution: {integrity: sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-optional-catch-binding@7.28.6':
+ resolution: {integrity: sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-optional-chaining@7.27.1':
+ resolution: {integrity: sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-optional-chaining@7.28.6':
+ resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-parameters@7.27.7':
+ resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-private-methods@7.28.6':
+ resolution: {integrity: sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-private-property-in-object@7.28.6':
+ resolution: {integrity: sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-display-name@7.28.0':
+ resolution: {integrity: sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx-development@7.27.1':
+ resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/plugin-transform-react-jsx-self@7.27.1':
resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
engines: {node: '>=6.9.0'}
@@ -1920,6 +2438,84 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/plugin-transform-react-jsx@7.28.6':
+ resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-pure-annotations@7.27.1':
+ resolution: {integrity: sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-regenerator@7.29.0':
+ resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-runtime@7.29.0':
+ resolution: {integrity: sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-shorthand-properties@7.27.1':
+ resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-spread@7.28.6':
+ resolution: {integrity: sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-sticky-regex@7.27.1':
+ resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-template-literals@7.27.1':
+ resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-typescript@7.28.6':
+ resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-unicode-regex@7.27.1':
+ resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/preset-react@7.28.5':
+ resolution: {integrity: sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/preset-typescript@7.27.1':
+ resolution: {integrity: sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/preset-typescript@7.28.5':
+ resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/runtime@7.27.1':
resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==}
engines: {node: '>=6.9.0'}
@@ -1932,6 +2528,10 @@ packages:
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'}
+ '@babel/template@7.28.6':
+ resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
+ engines: {node: '>=6.9.0'}
+
'@babel/traverse@7.27.1':
resolution: {integrity: sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==}
engines: {node: '>=6.9.0'}
@@ -1944,6 +2544,10 @@ packages:
resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==}
engines: {node: '>=6.9.0'}
+ '@babel/traverse@7.29.0':
+ resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
+ engines: {node: '>=6.9.0'}
+
'@babel/types@7.27.1':
resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==}
engines: {node: '>=6.9.0'}
@@ -1956,6 +2560,10 @@ packages:
resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==}
engines: {node: '>=6.9.0'}
+ '@babel/types@7.29.0':
+ resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
+ engines: {node: '>=6.9.0'}
+
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
@@ -2339,15 +2947,16 @@ packages:
'@effect/rpc': ^0.71.0
effect: ^3.18.1
+ '@egjs/hammerjs@2.0.17':
+ resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==}
+ engines: {node: '>=0.8.0'}
+
'@emnapi/core@1.10.0':
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
'@emnapi/core@1.5.0':
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
- '@emnapi/core@1.9.1':
- resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==}
-
'@emnapi/runtime@1.10.0':
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
@@ -2360,15 +2969,9 @@ packages:
'@emnapi/runtime@1.7.1':
resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
- '@emnapi/runtime@1.9.1':
- resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==}
-
'@emnapi/wasi-threads@1.1.0':
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
- '@emnapi/wasi-threads@1.2.0':
- resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
-
'@emnapi/wasi-threads@1.2.1':
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
@@ -3618,6 +4221,174 @@ packages:
resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@expo-google-fonts/material-symbols@0.4.38':
+ resolution: {integrity: sha512-IJkBtN1o8u9BW5fvSii1MyHPQ7Q0HxbWcVBvOrOzgMLpVtZw7R2w94wBTVR7kZwv3w1JNTESMmLA5Sqn1+Z36A==}
+
+ '@expo/cli@55.0.30':
+ resolution: {integrity: sha512-luWcCgompncWtCi1HqQfY32MVOuD0kUeARpr1Le1LeKVtZykjOwnz7YWXZo5zjISiD7L/gQnBNGVrRjvREsJqg==}
+ hasBin: true
+ peerDependencies:
+ expo: '*'
+ expo-router: '*'
+ react-native: '*'
+ peerDependenciesMeta:
+ expo-router:
+ optional: true
+ react-native:
+ optional: true
+
+ '@expo/code-signing-certificates@0.0.6':
+ resolution: {integrity: sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==}
+
+ '@expo/config-plugins@55.0.9':
+ resolution: {integrity: sha512-jLfpxru8dTo7eU0cqeTWuQav7byyjb37eF/mbXl1/3eTBHBvFU1VGxpeKxanUdTQAAjqzH8KGgWb0fWcce+z1w==}
+
+ '@expo/config-types@55.0.5':
+ resolution: {integrity: sha512-sCmSUZG4mZ/ySXvfyyBdhjivz8Q539X1NondwDdYG7s3SBsk+wsgPJzYsqgAG/P9+l0xWjUD2F+kQ1cAJ6NNLg==}
+
+ '@expo/config@55.0.17':
+ resolution: {integrity: sha512-Y3VaRg7Jllg3MhlUOTQqHm6/dttsqcjYlnS9enhAllZvPUpTHnRA4YPETtUZlxkdMJy6y3UZe986pd/KfJ6OTg==}
+
+ '@expo/devcert@1.2.1':
+ resolution: {integrity: sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA==}
+
+ '@expo/devtools@55.0.3':
+ resolution: {integrity: sha512-KoIDgo0NoXeWLsIcOdZqtAG/1LlsM+JL0DA3bo0vCYaOYTBLXi/ZvRBqa20Ub8D2vKLNa+FgRQW0gRg04Ps1Pg==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-native:
+ optional: true
+
+ '@expo/dom-webview@55.0.6':
+ resolution: {integrity: sha512-ZNm8tiNEZysxrr36J0x4mOCGyJDcaIvL/3tMxBz0VJIJDcV19xjuJAhJQxHovu+jKx6s9tRyEAINa1mdrzV39g==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
+ '@expo/env@2.1.2':
+ resolution: {integrity: sha512-RJtGFfj/ygO/6zcVbV3cckHf4THcEkv5IZft1GjCB3dfT6axvzvIwXE9EiQqQYmGHcQ+ZrvC8xZcIhiHba0pYg==}
+ engines: {node: '>=20.12.0'}
+
+ '@expo/fingerprint@0.16.7':
+ resolution: {integrity: sha512-BH8sicYOqZ1iBMwCVEGIz6uTTfylosjc49FoMmCYIzKOiYdiVehsfoYBwyfxwWIiya1VMhm1gv0cgOP8fxHpDw==}
+ hasBin: true
+
+ '@expo/image-utils@0.8.14':
+ resolution: {integrity: sha512-5Sn+jG4Cw+shC2wDMXoqSAJnvERbiwzHn05FpWtD5IBflfTIs5gUmjzwiGVyjOdlMSQhgRrw/AymPbmO9h9mpQ==}
+
+ '@expo/json-file@10.0.14':
+ resolution: {integrity: sha512-yWwBFywFv+SxkJp/pIzzA416JVYflNUh7pqQzgaA6nXDqRyK7KfrqVzk8PdUfDnqbBcaZZxpzNssfQZzp5KHrA==}
+
+ '@expo/local-build-cache-provider@55.0.13':
+ resolution: {integrity: sha512-Vg5BE10UL+0yg3BVtIeiSoeHU31Qe1m3UxhBPS478ACY1zzKuxZE30x2sym/B2OIWypjmPzXDRt8J9TOGFuFNw==}
+
+ '@expo/log-box@55.0.12':
+ resolution: {integrity: sha512-f9ARS8J60cq3LLNdIqmUjYwyerBzVS5Ecp7KjIf3GOIPjW0571rkcwLz4/U18l/1DeSkSzIkYsNl2TC9oTdWaQ==}
+ peerDependencies:
+ '@expo/dom-webview': ^55.0.6
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
+ '@expo/metro-config@55.0.21':
+ resolution: {integrity: sha512-pJ8G0uCxqA9KK+XCzXZF7ZI37rduD2l7Cun2e3rVAgB2yeOZagUD+VBvooU9QPiWx9e/7EbimH5/JP81JyhQlg==}
+ peerDependencies:
+ expo: '*'
+ peerDependenciesMeta:
+ expo:
+ optional: true
+
+ '@expo/metro-runtime@55.0.11':
+ resolution: {integrity: sha512-4KKi/jGrIEXi2YGu0hYTVr0CEeRJy5SXbCrz9+KDZkuD3ROwKNpM1DBawni5rhPVovFnR323HBck9GaxhnfrRw==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-dom: '*'
+ react-native: '*'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
+ '@expo/metro@55.1.1':
+ resolution: {integrity: sha512-/wfXo5hTuAVpVLG/4hzlmD9NBGJkzkmBEMm/4VICajYRbj7y8OmqqPWbbymzHiBiHB6tI9BnsyXpQM6zVZEECg==}
+
+ '@expo/osascript@2.4.3':
+ resolution: {integrity: sha512-wbuj3EebM7W9hN/Wp4xTzKd6rQ2zKJzAxkFxkOOwyysLp0HOAgQ4/5RINyoS241pZUX2rUHq7mAJ7pcCQ8U0Ow==}
+ engines: {node: '>=12'}
+
+ '@expo/package-manager@1.10.5':
+ resolution: {integrity: sha512-nCP9Mebfl3jvOr0/P6VAuyah6PAtun+aihIL2zAtuE8uSe94JWkVZ7051i0MUVO+y3gFpBqnr8IIH5ch+VJjHA==}
+
+ '@expo/plist@0.5.3':
+ resolution: {integrity: sha512-jz5oPcPDd3fygwVxwSwmO6wodTwm0Qa14NUyPy0ka7H8sFmCtNZUI2+DzVe/EXjOhq1FbEjrwl89gdlWYOnVjQ==}
+
+ '@expo/prebuild-config@55.0.18':
+ resolution: {integrity: sha512-2oKXyy5pyM87DJqXW5Z+Sakle6rApFFtpPhWOiNsOdoh6rOAD+EqVgyrs2OEEic8CE0tTt27w3SRfSZe/PZrxg==}
+ peerDependencies:
+ expo: '*'
+
+ '@expo/require-utils@55.0.5':
+ resolution: {integrity: sha512-U4K/CQ2VpXuwfNGsN+daKmYOt15hCP8v/pXaYH6eut7kdYZo6SfJ1yr67BIcJ+1Gzzs+QzTxswAZChKpXmceyw==}
+ peerDependencies:
+ typescript: ^5.0.0 || ^5.0.0-0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@expo/router-server@55.0.16':
+ resolution: {integrity: sha512-LvAdrm039nQBG+95+ff5Rc4CsBuoc/giDhjQrgxB9lKJqC/ZTq1xbwfEZFNq6yokX6fOCs/vlxdhmSkOjMIrvg==}
+ peerDependencies:
+ '@expo/metro-runtime': ^55.0.11
+ expo: '*'
+ expo-constants: ^55.0.16
+ expo-font: ^55.0.7
+ expo-router: '*'
+ expo-server: ^55.0.9
+ react: '*'
+ react-dom: '*'
+ react-server-dom-webpack: ~19.0.1 || ~19.1.2 || ~19.2.1
+ peerDependenciesMeta:
+ '@expo/metro-runtime':
+ optional: true
+ expo-router:
+ optional: true
+ react-dom:
+ optional: true
+ react-server-dom-webpack:
+ optional: true
+
+ '@expo/schema-utils@55.0.4':
+ resolution: {integrity: sha512-65IdeeE8dAZR3n3J5Eq7LYiQ8BFGeEYCWPBCzycvafL7PkskbCyIclTQarRwf/HXFoRvezKCjaLwy/8v9Prk6g==}
+
+ '@expo/sdk-runtime-versions@1.0.0':
+ resolution: {integrity: sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==}
+
+ '@expo/spawn-async@1.7.2':
+ resolution: {integrity: sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==}
+ engines: {node: '>=12'}
+
+ '@expo/sudo-prompt@9.3.2':
+ resolution: {integrity: sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==}
+
+ '@expo/vector-icons@15.1.1':
+ resolution: {integrity: sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw==}
+ peerDependencies:
+ expo-font: '>=14.0.4'
+ react: '*'
+ react-native: '*'
+
+ '@expo/ws-tunnel@1.0.6':
+ resolution: {integrity: sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q==}
+
+ '@expo/xcpretty@4.4.4':
+ resolution: {integrity: sha512-4aQzz9vgxcNXFfo/iyNgDDYfsU5XGKKxWxZopw0cVotHiW+U8IJbIxMaxsINs6bHhtkG3StKNPcOrn3eBuxKPw==}
+ hasBin: true
+
'@fastify/busboy@2.1.1':
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
engines: {node: '>=14'}
@@ -4024,10 +4795,54 @@ packages:
'@isaacs/string-locale-compare@1.1.0':
resolution: {integrity: sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==}
+ '@isaacs/ttlcache@1.4.1':
+ resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==}
+ engines: {node: '>=12'}
+
+ '@istanbuljs/load-nyc-config@1.1.0':
+ resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
+ engines: {node: '>=8'}
+
'@istanbuljs/schema@0.1.3':
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
engines: {node: '>=8'}
+ '@jest/create-cache-key-function@29.7.0':
+ resolution: {integrity: sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ '@jest/diff-sequences@30.4.0':
+ resolution: {integrity: sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jest/environment@29.7.0':
+ resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ '@jest/fake-timers@29.7.0':
+ resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ '@jest/get-type@30.1.0':
+ resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jest/schemas@29.6.3':
+ resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ '@jest/schemas@30.4.1':
+ resolution: {integrity: sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ '@jest/transform@29.7.0':
+ resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ '@jest/types@29.6.3':
+ resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -5151,6 +5966,9 @@ packages:
'@radix-ui/primitive@1.1.2':
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
+ '@radix-ui/primitive@1.1.3':
+ resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
+
'@radix-ui/react-arrow@1.1.6':
resolution: {integrity: sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw==}
peerDependencies:
@@ -5493,6 +6311,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-presence@1.1.5':
+ resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
+ 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
+
'@radix-ui/react-primitive@1.0.0':
resolution: {integrity: sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==}
peerDependencies:
@@ -5525,6 +6356,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-roving-focus@1.1.11':
+ resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
+ 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
+
'@radix-ui/react-roving-focus@1.1.9':
resolution: {integrity: sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==}
peerDependencies:
@@ -5600,6 +6444,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-tabs@1.1.13':
+ resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==}
+ 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
+
'@radix-ui/react-tooltip@1.2.6':
resolution: {integrity: sha512-zYb+9dc9tkoN2JjBDIIPLQtk3gGyz8FMKoqYTb8EMVQ5a5hBcdHPECrsZVI4NpPAUOixhkoqg7Hj5ry5USowfA==}
peerDependencies:
@@ -5859,6 +6716,149 @@ packages:
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
+ '@react-native/assets-registry@0.83.6':
+ resolution: {integrity: sha512-iljb4ue1yWJ3EhySz7EjV6CzSVrI2uNtR8BI2jzP5+QS5E4Cl3fdIJRmVwDEx1pu8uE97PGEusGRHnoaZ9Q3jg==}
+ engines: {node: '>= 20.19.4'}
+
+ '@react-native/babel-plugin-codegen@0.83.6':
+ resolution: {integrity: sha512-qfRXsHGeucT5c6mK+8Q7v4Ly3zmygfVmFlEtkiq7q07W1OTreld6nib4rJ/DBEeNiKBoBTuHjWliYGNuDjLFQA==}
+ engines: {node: '>= 20.19.4'}
+
+ '@react-native/babel-plugin-codegen@0.85.3':
+ resolution: {integrity: sha512-Wc94zGfeFG8Njf9SHMPfYZP04kjigkOps6F1TYTvd7ZVXuGxqseCDgxc50LWcOhOCLypI9n3oVVqz81C3p44ZA==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+
+ '@react-native/babel-preset@0.83.6':
+ resolution: {integrity: sha512-4/fXFDUvGOObETZq4+SUFkafld6OGgQWut5cQiqVghlhCB5z/p2lVhPgEUr/aTxTzeS3AmN+ztC+GpYPQ7tsTw==}
+ engines: {node: '>= 20.19.4'}
+ peerDependencies:
+ '@babel/core': '*'
+
+ '@react-native/babel-preset@0.85.3':
+ resolution: {integrity: sha512-fD7fxEhkJB/aF57tWoXjaAWpklfrExYZS3k6aXPP3BQ77DZY7gvf/b7dbirwjID6NVnP1JDRJyTuPBGr0K/vlw==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+ peerDependencies:
+ '@babel/core': '*'
+
+ '@react-native/codegen@0.83.6':
+ resolution: {integrity: sha512-doB/Pq6Cf6IjF3wlQXTIiZOnsX9X8mEEk+CdGfyuCwZjWrf7IB8KaZEXXckJmfUcIwvJ9u/a72ZoTTCIoxAc9A==}
+ engines: {node: '>= 20.19.4'}
+ peerDependencies:
+ '@babel/core': '*'
+
+ '@react-native/codegen@0.85.3':
+ resolution: {integrity: sha512-/JkS1lGLyzBWP1FbgDwaqEf7qShIC6pUC1M0a/YMAd/v4iqR24MRkQWe7jkYvcBQ2LpEhs5NGE9InhxSv21zCA==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+ peerDependencies:
+ '@babel/core': '*'
+
+ '@react-native/community-cli-plugin@0.83.6':
+ resolution: {integrity: sha512-Mko6mywoHYJmpBnjwAC95vQWaUUh//71knFadH0BrhHDq2m7i/IrpLwcQsPAy8855ucXflBs5zQyGTpNbPBAaw==}
+ engines: {node: '>= 20.19.4'}
+ peerDependencies:
+ '@react-native-community/cli': '*'
+ '@react-native/metro-config': '*'
+ peerDependenciesMeta:
+ '@react-native-community/cli':
+ optional: true
+ '@react-native/metro-config':
+ optional: true
+
+ '@react-native/debugger-frontend@0.83.6':
+ resolution: {integrity: sha512-TyWXEpAjVundrc87fPWg91piOUg75+X9iutcfDe7cO3NrAEYCsl7Z09rKHuiAGkxfG9/rFD13dPsYIixUFkSFA==}
+ engines: {node: '>= 20.19.4'}
+
+ '@react-native/debugger-shell@0.83.6':
+ resolution: {integrity: sha512-684TJMBCU0l0ZjJWzrnK0HH+ERaM9KLyxyArE1k7BrP+gVl4X9GO0Pi94RoInOxvW/nyV65sOU6Ip1F3ygS0cg==}
+ engines: {node: '>= 20.19.4'}
+
+ '@react-native/dev-middleware@0.83.6':
+ resolution: {integrity: sha512-22xoddLTelpcVnF385SNH2hdP7X2av5pu7yRl/WnM5jBznbcl0+M9Ce94cj+WVeomsoUF/vlfuB0Ooy+RMlRiA==}
+ engines: {node: '>= 20.19.4'}
+
+ '@react-native/gradle-plugin@0.83.6':
+ resolution: {integrity: sha512-5prXv7WWR1RgZ/kWGZP+mi7/y/IE2ymfOHIZO5Pv14tMOmRAcQSgSYogcRmOiWw5mJs2K0UFeMiQD49ZO9oCug==}
+ engines: {node: '>= 20.19.4'}
+
+ '@react-native/js-polyfills@0.83.6':
+ resolution: {integrity: sha512-VSev0LV2i5X0ibduHBSLqKj0YU2F+waCgjl2uvaGHMGCSV1ZRKNFX/vJFqvLwjvdzLbkAZoFT1Rg7k7jDv44UA==}
+ engines: {node: '>= 20.19.4'}
+
+ '@react-native/js-polyfills@0.85.3':
+ resolution: {integrity: sha512-U2+aMshIXf1uFn77tpBb/xhHWB9vkVrMpt7kkucAugF8hJKYTDGB587X7WwelHduK2KBfhl4giSv0rzZGoef9A==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+
+ '@react-native/metro-babel-transformer@0.85.3':
+ resolution: {integrity: sha512-omuKq+r7jM4XvCMIlNMPP7Up3SyB8o5EAdZtF7YXniKyq7UOMBqhYHFqgsdOXr0lT+3ADf7VCJG3sb82jlBrrQ==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+ peerDependencies:
+ '@babel/core': '*'
+
+ '@react-native/metro-config@0.85.3':
+ resolution: {integrity: sha512-sVo6HepUmCcpdfozEf91lA0FjpLNNZYu/Zi9FiYiAQTK8pzATXDVTqhvdxpFrQn435p5eUTSbllvbH/KN+bnyA==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+
+ '@react-native/normalize-colors@0.74.89':
+ resolution: {integrity: sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==}
+
+ '@react-native/normalize-colors@0.83.6':
+ resolution: {integrity: sha512-bTM24b5v4qN3h52oflnv+OujFORn/kVi06WaWhnQQw14/ycilPqIsqsa+DpIBqdBrXxvLa9fXtCRrQtGATZCEw==}
+
+ '@react-native/virtualized-lists@0.83.6':
+ resolution: {integrity: sha512-gNSFXeb4P7qHtauLvl+zESroULIyX6Ltpvau3dhwy/QmfanBv0KUcrIU/7aVXxtWcXgp+54oWJyu2LIrsZ9+LQ==}
+ engines: {node: '>= 20.19.4'}
+ peerDependencies:
+ '@types/react': ^19.2.0
+ react: '*'
+ react-native: '*'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@react-navigation/bottom-tabs@7.16.1':
+ resolution: {integrity: sha512-wjFATJmbq0K8B96Ax0JcK2+Eu7syfYvQ5qUd/tgcv8JuCYLwKKqojJMAl31qdjpKqFG09pQ6TSdEDHOek60CAA==}
+ peerDependencies:
+ '@react-navigation/native': ^7.2.4
+ react: '>= 18.2.0'
+ react-native: '*'
+ react-native-safe-area-context: '>= 4.0.0'
+ react-native-screens: '>= 4.0.0'
+
+ '@react-navigation/core@7.17.4':
+ resolution: {integrity: sha512-Rv9E2oNNQEkPGpmu9q+vJwGJRSQR6LBg5L+Yo1QHjtwGbHUbjkIKOdYymDZoZYgNzX2OD4rAIlfuzbDKa3cCeA==}
+ peerDependencies:
+ react: '>= 18.2.0'
+
+ '@react-navigation/elements@2.9.18':
+ resolution: {integrity: sha512-mKEvDr6CkCVYZSb8W9WubNseihL+1c8M7ktZJCTCbMk8rQgdQfkdRNwpSUQKspdGpUHCb9cyzvaiuzl1NtjVgw==}
+ peerDependencies:
+ '@react-native-masked-view/masked-view': '>= 0.2.0'
+ '@react-navigation/native': ^7.2.4
+ react: '>= 18.2.0'
+ react-native: '*'
+ react-native-safe-area-context: '>= 4.0.0'
+ peerDependenciesMeta:
+ '@react-native-masked-view/masked-view':
+ optional: true
+
+ '@react-navigation/native-stack@7.15.1':
+ resolution: {integrity: sha512-kNrJggwoB/onC0MpZIuZ6qaqeAziFchz+W9txBzhd6qbWmB1OkPVUnu6fWgc6BQc7MeMf59djVmqgX+6kJU1Ug==}
+ peerDependencies:
+ '@react-navigation/native': ^7.2.4
+ react: '>= 18.2.0'
+ react-native: '*'
+ react-native-safe-area-context: '>= 4.0.0'
+ react-native-screens: '>= 4.0.0'
+
+ '@react-navigation/native@7.2.4':
+ resolution: {integrity: sha512-eWC2D3JjhYLId2fVTZhhCiUpWIaPhO9XyEb7Wq8ElmOHyIODlbOzgZ0rKia02OIsDKr9BzZl2sK1dL70yMxDaw==}
+ peerDependencies:
+ react: '>= 18.2.0'
+ react-native: '*'
+
+ '@react-navigation/routers@7.5.5':
+ resolution: {integrity: sha512-9/hhMte12Kgu+pMnLfA4EWJ0OQmIEAMVMX06FPH2yGkEQSQ3JhhCN/GkcRikzQhtEi97VYYQA15umptBUShcOQ==}
+
'@reduxjs/toolkit@2.10.1':
resolution: {integrity: sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==}
peerDependencies:
@@ -6356,6 +7356,13 @@ packages:
'@shinyoshiaki/jspack@0.0.6':
resolution: {integrity: sha512-SdsNhLjQh4onBlyPrn4ia1Pdx5bXT88G/LIEpOYAjx2u4xeY/m/HB5yHqlkJB1uQR3Zw4R3hBWLj46STRAN0rg==}
+ '@shopify/flash-list@2.0.2':
+ resolution: {integrity: sha512-zhlrhA9eiuEzja4wxVvotgXHtqd3qsYbXkQ3rsBfOgbFA9BVeErpDE/yEwtlIviRGEqpuFj/oU5owD6ByaNX+w==}
+ peerDependencies:
+ '@babel/runtime': '*'
+ react: '*'
+ react-native: '*'
+
'@sigstore/bundle@2.3.2':
resolution: {integrity: sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==}
engines: {node: ^16.14.0 || >=18.0.0}
@@ -6380,6 +7387,12 @@ packages:
resolution: {integrity: sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==}
engines: {node: ^16.14.0 || >=18.0.0}
+ '@sinclair/typebox@0.27.10':
+ resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==}
+
+ '@sinclair/typebox@0.34.49':
+ resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==}
+
'@sindresorhus/is@4.6.0':
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
engines: {node: '>=10'}
@@ -6396,6 +7409,12 @@ packages:
resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==}
engines: {node: '>=18'}
+ '@sinonjs/commons@3.0.1':
+ resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==}
+
+ '@sinonjs/fake-timers@10.3.0':
+ resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==}
+
'@smithy/abort-controller@4.0.2':
resolution: {integrity: sha512-Sl/78VDtgqKxN2+1qduaVE140XF+Xg+TafkncspwM4jFP/LHr76ZHmIY/y3V1M0mMLNk+Je6IGbzxy23RSToMw==}
engines: {node: '>=18.0.0'}
@@ -7778,6 +8797,18 @@ packages:
resolution: {integrity: sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==}
engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
+ '@testing-library/react-native@13.3.3':
+ resolution: {integrity: sha512-k6Mjsd9dbZgvY4Bl7P1NIpePQNi+dfYtlJ5voi9KQlynxSyQkfOgJmYGCYmw/aSgH/rUcFvG8u5gd4npzgRDyg==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ jest: '>=29.0.0'
+ react: '>=18.2.0'
+ react-native: '>=0.71'
+ react-test-renderer: '>=18.2.0'
+ peerDependenciesMeta:
+ jest:
+ optional: true
+
'@testing-library/user-event@14.5.2':
resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==}
engines: {node: '>=12', npm: '>=6'}
@@ -7970,6 +9001,12 @@ packages:
'@types/google-protobuf@3.15.12':
resolution: {integrity: sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==}
+ '@types/graceful-fs@4.1.9':
+ resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
+
+ '@types/hammerjs@2.0.46':
+ resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==}
+
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
@@ -7982,6 +9019,15 @@ packages:
'@types/http-errors@2.0.4':
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
+ '@types/istanbul-lib-coverage@2.0.6':
+ resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
+
+ '@types/istanbul-lib-report@3.0.3':
+ resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==}
+
+ '@types/istanbul-reports@3.0.4':
+ resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==}
+
'@types/js-cookie@3.0.6':
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
@@ -8070,6 +9116,9 @@ packages:
'@types/react-responsive-masonry@2.6.0':
resolution: {integrity: sha512-MF2ql1CjzOoL9fLWp6L3ABoyzBUP/YV71wyb3Fx+cViYNj7+tq3gDCllZHbLg1LQfGOQOEGbV2P7TOcUeGiR6w==}
+ '@types/react-test-renderer@19.1.0':
+ resolution: {integrity: sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==}
+
'@types/react-tooltip@4.2.4':
resolution: {integrity: sha512-UzjzmgY/VH3Str6DcAGTLMA1mVVhGOyARNTANExrirtp+JgxhaIOVDxq4TIRmpSi4voLv+w4HA9CC5GvhhCA0A==}
deprecated: This is a stub types definition. react-tooltip provides its own type definitions, so you do not need this installed.
@@ -8098,6 +9147,9 @@ packages:
'@types/shimmer@1.2.0':
resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==}
+ '@types/stack-utils@2.0.3':
+ resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
+
'@types/tmp@0.2.6':
resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==}
@@ -8125,6 +9177,12 @@ packages:
'@types/uuid@9.0.8':
resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
+ '@types/yargs-parser@21.0.3':
+ resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
+
+ '@types/yargs@17.0.35':
+ resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==}
+
'@types/yauzl@2.10.3':
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
@@ -8724,6 +9782,14 @@ packages:
resolution: {integrity: sha512-siPY6BD5dQ2SZPl3I0OZBHL27ZqZvLEosObsZRQ1NUB8qcxegwt0T9eKtV96JMFQpIz1elhkzqOg4c/Ri6Dp9A==}
engines: {node: ^14.14.0 || >=16.0.0}
+ '@xmldom/xmldom@0.8.13':
+ resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
+ engines: {node: '>=10.0.0'}
+
+ '@xmldom/xmldom@0.9.10':
+ resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==}
+ engines: {node: '>=14.6'}
+
'@xtuc/ieee754@1.2.0':
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
@@ -8849,6 +9915,9 @@ packages:
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
+ anser@1.4.10:
+ resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==}
+
ansi-align@3.0.1:
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
@@ -8864,6 +9933,10 @@ packages:
resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==}
engines: {node: '>=18'}
+ ansi-regex@4.1.1:
+ resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==}
+ engines: {node: '>=6'}
+
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
@@ -9006,6 +10079,9 @@ packages:
as-table@1.0.55:
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
+ asap@2.0.6:
+ resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
+
asn1js@3.0.6:
resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==}
engines: {node: '>=12.0.0'}
@@ -9099,11 +10175,84 @@ packages:
babel-dead-code-elimination@1.0.10:
resolution: {integrity: sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==}
+ babel-jest@29.7.0:
+ resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ peerDependencies:
+ '@babel/core': ^7.8.0
+
+ babel-plugin-istanbul@6.1.1:
+ resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==}
+ engines: {node: '>=8'}
+
+ babel-plugin-jest-hoist@29.6.3:
+ resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
babel-plugin-jsx-dom-expressions@0.39.8:
resolution: {integrity: sha512-/MVOIIjonylDXnrWmG23ZX82m9mtKATsVHB7zYlPfDR9Vdd/NBE48if+wv27bSkBtyO7EPMUlcUc4J63QwuACQ==}
peerDependencies:
'@babel/core': ^7.20.12
+ babel-plugin-polyfill-corejs2@0.4.17:
+ resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ babel-plugin-polyfill-corejs3@0.13.0:
+ resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ babel-plugin-polyfill-regenerator@0.6.8:
+ resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ babel-plugin-react-compiler@1.0.0:
+ resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==}
+
+ babel-plugin-react-native-web@0.21.2:
+ resolution: {integrity: sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==}
+
+ babel-plugin-syntax-hermes-parser@0.32.0:
+ resolution: {integrity: sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==}
+
+ babel-plugin-syntax-hermes-parser@0.32.1:
+ resolution: {integrity: sha512-HgErPZTghW76Rkq9uqn5ESeiD97FbqpZ1V170T1RG2RDp+7pJVQV2pQJs7y5YzN0/gcT6GM5ci9apRnIwuyPdQ==}
+
+ babel-plugin-syntax-hermes-parser@0.33.3:
+ resolution: {integrity: sha512-/Z9xYdaJ1lC0pT9do6TqCqhOSLfZ5Ot8D5za1p+feEfWYupCOfGbhhEXN9r2ZgJtDNUNRw/Z+T2CvAGKBqtqWA==}
+
+ babel-plugin-transform-flow-enums@0.0.2:
+ resolution: {integrity: sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==}
+
+ babel-preset-current-node-syntax@1.2.0:
+ resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==}
+ peerDependencies:
+ '@babel/core': ^7.0.0 || ^8.0.0-0
+
+ babel-preset-expo@55.0.21:
+ resolution: {integrity: sha512-anXoUZBcxydLdVs2L+r3bWKGUvZv2FtgOl8xRJ12i/YfKICBpwTGZWSTiEYTqBByZ6GkA3mE9+3TW97X2ocFTQ==}
+ peerDependencies:
+ '@babel/runtime': ^7.20.0
+ expo: '*'
+ expo-widgets: ^55.0.17
+ react-refresh: '>=0.14.0 <1.0.0'
+ peerDependenciesMeta:
+ '@babel/runtime':
+ optional: true
+ expo:
+ optional: true
+ expo-widgets:
+ optional: true
+
+ babel-preset-jest@29.6.3:
+ resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
babel-preset-solid@1.9.6:
resolution: {integrity: sha512-HXTK9f93QxoH8dYn1M2mJdOlWgMsR88Lg/ul6QCZGkNTktjTE5HAf93YxQumHoCudLEtZrU1cFCMFOVho6GqFg==}
peerDependencies:
@@ -9134,8 +10283,9 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
- baseline-browser-mapping@2.8.16:
- resolution: {integrity: sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==}
+ baseline-browser-mapping@2.10.30:
+ resolution: {integrity: sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg==}
+ engines: {node: '>=6.0.0'}
hasBin: true
before-after-hook@2.2.3:
@@ -9151,6 +10301,10 @@ packages:
bezier-easing@2.1.0:
resolution: {integrity: sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==}
+ big-integer@1.6.52:
+ resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
+ engines: {node: '>=0.6'}
+
bin-links@4.0.4:
resolution: {integrity: sha512-cMtq4W5ZsEwcutJrVId+a/tjt8GSbS+h0oNkdl6+6rBuEv8Ot33Bevj5KPm40t309zuhVic8NjpuL42QCiJWWA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -9193,6 +10347,9 @@ packages:
resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
engines: {node: '>=18'}
+ boolbase@1.0.0:
+ resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+
bottleneck@2.19.5:
resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==}
@@ -9203,6 +10360,17 @@ packages:
resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==}
engines: {node: '>=18'}
+ bplist-creator@0.1.0:
+ resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==}
+
+ bplist-parser@0.3.1:
+ resolution: {integrity: sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==}
+ engines: {node: '>= 5.10.0'}
+
+ bplist-parser@0.3.2:
+ resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==}
+ engines: {node: '>= 5.10.0'}
+
brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
@@ -9230,6 +10398,14 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
+ browserslist@4.28.2:
+ resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ bser@2.1.1:
+ resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
+
buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
@@ -9337,6 +10513,14 @@ packages:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
+ camelcase@5.3.1:
+ resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
+ engines: {node: '>=6'}
+
+ camelcase@6.3.0:
+ resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
+ engines: {node: '>=10'}
+
camelcase@8.0.0:
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
engines: {node: '>=16'}
@@ -9347,6 +10531,9 @@ packages:
caniuse-lite@1.0.30001750:
resolution: {integrity: sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==}
+ caniuse-lite@1.0.30001793:
+ resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
+
canvas-confetti@1.9.3:
resolution: {integrity: sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==}
@@ -9448,10 +10635,25 @@ packages:
'@chromatic-com/playwright':
optional: true
+ chrome-launcher@0.15.2:
+ resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==}
+ engines: {node: '>=12.13.0'}
+ hasBin: true
+
chrome-trace-event@1.0.4:
resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==}
engines: {node: '>=6.0'}
+ chromium-edge-launcher@0.2.0:
+ resolution: {integrity: sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==}
+
+ ci-info@2.0.0:
+ resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==}
+
+ ci-info@3.9.0:
+ resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
+ engines: {node: '>=8'}
+
citty@0.1.6:
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
@@ -9476,6 +10678,10 @@ packages:
resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
engines: {node: '>=10'}
+ cli-cursor@2.1.0:
+ resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==}
+ engines: {node: '>=4'}
+
cli-cursor@5.0.0:
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
engines: {node: '>=18'}
@@ -9568,6 +10774,10 @@ packages:
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
engines: {node: '>=14'}
+ commander@12.1.0:
+ resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
+ engines: {node: '>=18'}
+
commander@13.1.0:
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
engines: {node: '>=18'}
@@ -9583,6 +10793,10 @@ packages:
resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==}
engines: {node: '>= 6'}
+ commander@7.2.0:
+ resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
+ engines: {node: '>= 10'}
+
commander@8.3.0:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'}
@@ -9607,6 +10821,14 @@ packages:
resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==}
engines: {node: '>= 14'}
+ compressible@2.0.18:
+ resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
+ engines: {node: '>= 0.6'}
+
+ compression@1.8.1:
+ resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==}
+ engines: {node: '>= 0.8.0'}
+
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -9625,6 +10847,10 @@ packages:
confbox@0.2.2:
resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==}
+ connect@3.7.0:
+ resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==}
+ engines: {node: '>= 0.10.0'}
+
consola@3.4.2:
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
engines: {node: ^14.18.0 || >=16.10.0}
@@ -9675,6 +10901,9 @@ packages:
cookies-next@4.3.0:
resolution: {integrity: sha512-XxeCwLR30cWwRd94sa9X5lRCDLVujtx73tv+N0doQCFIDl83fuuYdxbu/WQUt9aSV7EJx7bkMvJldjvzuFqr4w==}
+ core-js-compat@3.49.0:
+ resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==}
+
core-js@3.42.0:
resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==}
@@ -9731,6 +10960,20 @@ packages:
crossws@0.3.5:
resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==}
+ css-in-js-utils@3.1.0:
+ resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==}
+
+ css-select@5.2.2:
+ resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
+
+ css-tree@1.1.3:
+ resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==}
+ engines: {node: '>=8.0.0'}
+
+ css-what@6.2.2:
+ resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
+ engines: {node: '>= 6'}
+
css.escape@1.5.1:
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
@@ -9927,6 +11170,10 @@ packages:
decode-named-character-reference@1.1.0:
resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==}
+ decode-uri-component@0.2.2:
+ resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
+ engines: {node: '>=0.10'}
+
decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
@@ -10105,6 +11352,9 @@ packages:
resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==}
engines: {node: '>=6'}
+ dnssd-advertise@1.1.4:
+ resolution: {integrity: sha512-AmGyK9WpNf06WeP5TjHZq/wNzP76OuEeaiTlKr9E/EEelYLczywUKoqRz+DPRq/ErssjT4lU+/W7wzJW+7K/ZA==}
+
doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
@@ -10319,6 +11569,9 @@ packages:
electron-to-chromium@1.5.234:
resolution: {integrity: sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==}
+ electron-to-chromium@1.5.357:
+ resolution: {integrity: sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g==}
+
emoji-regex-xs@1.0.0:
resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==}
@@ -10507,6 +11760,10 @@ packages:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
+ escape-string-regexp@2.0.0:
+ resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==}
+ engines: {node: '>=8'}
+
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@@ -10854,6 +12111,226 @@ packages:
resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==}
engines: {node: '>=12.0.0'}
+ expo-asset@55.0.17:
+ resolution: {integrity: sha512-pK9HHJuFqjE8kDUcbMFsZj3Cz8WdXpvZHZmYl7ouFQp59P83BvHln6VnqPDGlO+/4929G0Lm8ZUzbONuNRhi9w==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
+ expo-clipboard@55.0.13:
+ resolution: {integrity: sha512-PrOmmuVsGW4bAkNQmGKtxMXj3invsfN+jfIKmQxHwE/dn7ODqwFWviUTa+PMUjP3XZmYCDLyu/i0GLeu7HF9Ew==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
+ expo-constants@55.0.16:
+ resolution: {integrity: sha512-Z15/No94UHoogD+pulxjudGAeOHTEIWZgb/vnX48Wx5D+apWTeCbnKxQZZtGQlosvduYL5kaic2/W8U+NHfBQQ==}
+ peerDependencies:
+ expo: '*'
+ react-native: '*'
+
+ expo-dev-client@55.0.34:
+ resolution: {integrity: sha512-IiQcIyzE/ixWtOa73XGf/7bsIN4DRnMvrmheCvCkqFIUv/mi+RLQt9D+xRRVbIwfnmjgDCjGxOLJVzFEcUbcIg==}
+ peerDependencies:
+ expo: '*'
+
+ expo-dev-launcher@55.0.35:
+ resolution: {integrity: sha512-Cfdx4exreS9J7zLe9iE+ARItpse1ixjdXn+5W0ZdqCYdSrN+AabKtHmevXOYImBn+R1aXdA8UGkJ/W6OoCXjNQ==}
+ peerDependencies:
+ expo: '*'
+
+ expo-dev-menu-interface@55.0.2:
+ resolution: {integrity: sha512-DomUNvGzY/xliwnMdbAYY780sCv19N7zIbifc0ClcoCzJZpNSCkvJ2qGIFRPyM/7DmqmlHGCKi8di7kYYLKNEg==}
+ peerDependencies:
+ expo: '*'
+
+ expo-dev-menu@55.0.29:
+ resolution: {integrity: sha512-dzKE+2Ag8nHhTgSetjDVR+u4UvgaCfRdQrl6tJyFbeYHJ2CZVxhRsMfH4ULQxF5ry/bJeSxZ9dbQWizGnXP9mg==}
+ peerDependencies:
+ expo: '*'
+
+ expo-document-picker@55.0.13:
+ resolution: {integrity: sha512-IhswJElhdzs3fKDEKW8KXYRoFkWGEsXRMYAZT46Yo56zqqy8yQXrczo33RSwD2hFzNQBdLT97SJL9N311UyS3g==}
+ peerDependencies:
+ expo: '*'
+
+ expo-file-system@55.0.20:
+ resolution: {integrity: sha512-sBCHhNlCT3EiqCcE6xSbyvOLUAlKx7+p0qjo+c+UPyC/gMrXUdva99g25uptM+fEMwy2co25MUQQ0U0guQLOQA==}
+ peerDependencies:
+ expo: '*'
+ react-native: '*'
+
+ expo-font@55.0.7:
+ resolution: {integrity: sha512-oH39Xb+3i6Y69b7YRP+P+5WLx7621t+ep/RAgLwJJYpTjs7CnSohUG+873rEtqsTAuQGi63ms7x9ZeHj1E9LYw==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
+ expo-glass-effect@55.0.11:
+ resolution: {integrity: sha512-wqq7GUOqSkfoFJzreZvBG0jzjsq5c582m3glhWSjcmIuByxXXWp6j6GY6hyFuYKzpOXhbuvusVxGCQi0yWnp3g==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
+ expo-image-loader@55.0.0:
+ resolution: {integrity: sha512-NOjp56wDrfuA5aiNAybBIjqIn1IxKeGJ8CECWZncQ/GzjZfyTYAHTCyeApYkdKkMBLHINzI4BbTGSlbCa0fXXQ==}
+ peerDependencies:
+ expo: '*'
+
+ expo-image-picker@55.0.20:
+ resolution: {integrity: sha512-lfWt/0rPWdKz8AdDEGmGHZIJSNlVc720Dlx5bfou10FU16ZV5wAbTU63nm2jkXd8hbXke4a/2Ha1dzxCVA+LQQ==}
+ peerDependencies:
+ expo: '*'
+
+ expo-image@55.0.10:
+ resolution: {integrity: sha512-We+vq/Z8jy8zmGxcOP8vrhiWkkwyXFdSks8cSlPi0bpu6D0Ei6l9Nj2xHWCD+yoENh92aCEe1+QRujAwXbogGA==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+ react-native-web: '*'
+ peerDependenciesMeta:
+ react-native-web:
+ optional: true
+
+ expo-json-utils@55.0.2:
+ resolution: {integrity: sha512-QJMOZOPOG7CTnKcrdVaiummn2va1MCO56z++eyWkDv3GBRODldM6MFMDf/jTREWthFc2Nxo6TuyWRrEV9S6n/Q==}
+
+ expo-keep-awake@55.0.8:
+ resolution: {integrity: sha512-PfIpMfM+STOBwkR5XOE+yVtER86c44MD+W8QD8JxuO0sT9pF7Y1SJYakWlpvX8xsGA+bjKLxftm9403s9kQhKA==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+
+ expo-linking@55.0.15:
+ resolution: {integrity: sha512-/RQh2vkNqV8Bim9Owm/evVqn2fqTvCDYHkpYPoSKbLAdydSGdHC2xZNw7Odl4wu1i1/3L4Xz//LKd3NsPWYWBQ==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
+ expo-manifests@55.0.17:
+ resolution: {integrity: sha512-vKZvFivX3usVJKfBODKQcFHso0g38zlGbRGqGAppz+il0zKvG6umpJ47OZbzLod7iJpjd+ZDD2AGuOxacixonA==}
+ peerDependencies:
+ expo: '*'
+
+ expo-media-library@55.0.17:
+ resolution: {integrity: sha512-x/8bdVZAjjB/yitlYZs87qXxxCpJdQBhJ0juXcGHPXCkijNz2sef6BmnbbK5FXg8jxf5nHkG0bIUQgo223hOvw==}
+ peerDependencies:
+ expo: '*'
+ react-native: '*'
+
+ expo-modules-autolinking@55.0.22:
+ resolution: {integrity: sha512-13x32V0HMHJDjND4K/gU2lQIZNxYn5S5rFzujqHmnXvOO6WGrVVELpk/0p5FmBfeuQ7GGFsATbhazQk+FeukUw==}
+ hasBin: true
+
+ expo-modules-core@55.0.25:
+ resolution: {integrity: sha512-yXpfg7aHLbuqoXocK34Vua6Aey5SCyqLygAsXAMbul9P8vfBjLpaOPiTJ5cLVF7Drfq8ownqVJO6qpGEtZ6GOw==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+ react-native-worklets: 0.7.4
+ peerDependenciesMeta:
+ react-native-worklets:
+ optional: true
+
+ expo-router@55.0.14:
+ resolution: {integrity: sha512-rOn/wosp2hAPM+O2o41hnarbP5Zqv9UkHWa31KoSoiOme1tpmZd2yc93XtRAtzP0P5E5xzqq7a2rbEAarpP5XA==}
+ peerDependencies:
+ '@expo/log-box': 55.0.12
+ '@expo/metro-runtime': ^55.0.11
+ '@react-navigation/drawer': ^7.9.4
+ '@testing-library/react-native': '>= 13.2.0'
+ expo: '*'
+ expo-constants: ^55.0.16
+ expo-linking: ^55.0.15
+ react: '*'
+ react-dom: '*'
+ react-native: '*'
+ react-native-gesture-handler: '*'
+ react-native-reanimated: '*'
+ react-native-safe-area-context: '>= 5.4.0'
+ react-native-screens: '*'
+ react-native-web: '*'
+ react-server-dom-webpack: ~19.0.4 || ~19.1.5 || ~19.2.4
+ peerDependenciesMeta:
+ '@react-navigation/drawer':
+ optional: true
+ '@testing-library/react-native':
+ optional: true
+ react-dom:
+ optional: true
+ react-native-gesture-handler:
+ optional: true
+ react-native-reanimated:
+ optional: true
+ react-native-web:
+ optional: true
+ react-server-dom-webpack:
+ optional: true
+
+ expo-secure-store@55.0.14:
+ resolution: {integrity: sha512-OKp9pDiTa4kgChop8+pTRJGBPhkJUcAxP5c6JbivNr4bmx3I+gKmAj1ov4KOXkY95TpWdHO+GQ4+0BgSY2P3JQ==}
+ peerDependencies:
+ expo: '*'
+
+ expo-server@55.0.9:
+ resolution: {integrity: sha512-N5Ipn1NwqaJzEm+G97o0Jbe4g/th3R/16N1DabnYryXKCiZwDkK13/w3VfGkQN9LOOaBP+JIRxGf4M8lQKPzyA==}
+ engines: {node: '>=20.16.0'}
+
+ expo-sharing@55.0.19:
+ resolution: {integrity: sha512-I1NC3ZPJvSpq8ptklUSUgh4xw9uxvqkqAhrdEHMr5qNxLEvCCReC+KFnySqamMjLKoZqqDJ2LeNnQxQxfauhfA==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
+ expo-symbols@55.0.8:
+ resolution: {integrity: sha512-Dg6BTu+fCWukdlh+3XYIr6NbqJWmK4aAQ6i6BInKnWU0ALuzVUJcMDq8Lk9bHok2hOh3OhzJqlCqEoBXPInIVQ==}
+ peerDependencies:
+ expo: '*'
+ expo-font: '*'
+ react: '*'
+ react-native: '*'
+
+ expo-updates-interface@55.1.6:
+ resolution: {integrity: sha512-evxNpagCkjT3lE6bGV570TFzRtKuIuLY8I37RYHoriXCJ+ZKCN1hbmklK29uAixya+BxGpeTI2K4FqYeJLvfrw==}
+ peerDependencies:
+ expo: '*'
+
+ expo-video@55.0.17:
+ resolution: {integrity: sha512-z2Fqg1WkctD2jpsUoMQU9y6jWYlV+Lwb7nIMB+3fOcKMVCiUwSWS9xq/MQnRImZe6t9gfRFVMvw2Xz8Lk/kUPw==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
+ expo-web-browser@55.0.16:
+ resolution: {integrity: sha512-eeGs3439ewO/Q56Pzg3qbAVZSE0oH/R7XW9VCXI59k0m78ZIYbBtPT4PMFL/+sBgRkXm546Lq/DFcJQPTOfXJg==}
+ peerDependencies:
+ expo: '*'
+ react-native: '*'
+
+ expo@55.0.24:
+ resolution: {integrity: sha512-nU95y+GIfD1dm9CSjsitDdltSU83dDqemxD1UUBxJPH8zKf7B5AdGVNyE6/jLWyCM/p/EmHfCeiqdrWCy9ljZA==}
+ hasBin: true
+ peerDependencies:
+ '@expo/dom-webview': '*'
+ '@expo/metro-runtime': '*'
+ react: '*'
+ react-native: '*'
+ react-native-webview: '*'
+ peerDependenciesMeta:
+ '@expo/dom-webview':
+ optional: true
+ '@expo/metro-runtime':
+ optional: true
+ react-native-webview:
+ optional: true
+
exponential-backoff@3.1.2:
resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==}
@@ -10953,6 +12430,20 @@ packages:
fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
+ fb-dotslash@0.5.8:
+ resolution: {integrity: sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==}
+ engines: {node: '>=20'}
+ hasBin: true
+
+ fb-watchman@2.0.2:
+ resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
+
+ fbjs-css-vars@1.0.2:
+ resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==}
+
+ fbjs@3.0.5:
+ resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==}
+
fd-slicer@1.1.0:
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
@@ -10980,6 +12471,9 @@ packages:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
+ fetch-nodeshim@0.4.10:
+ resolution: {integrity: sha512-m6I8ALe4L4XpdETy7MJZWs6L1IVMbjs99bwbpIKphxX+0CTns4IKDWJY0LWfr4YsFjfg+z1TjzTMU8lKl8rG0w==}
+
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
@@ -11031,10 +12525,18 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
+ filter-obj@1.1.0:
+ resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==}
+ engines: {node: '>=0.10.0'}
+
filter-obj@5.1.0:
resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==}
engines: {node: '>=14.16'}
+ finalhandler@1.1.2:
+ resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==}
+ engines: {node: '>= 0.8'}
+
finalhandler@1.3.2:
resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==}
engines: {node: '>= 0.8'}
@@ -11050,6 +12552,10 @@ packages:
resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==}
engines: {node: '>=18'}
+ find-up@4.1.0:
+ resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
+ engines: {node: '>=8'}
+
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
@@ -11080,6 +12586,9 @@ packages:
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+ flow-enums-runtime@0.0.6:
+ resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==}
+
fn.name@1.1.0:
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
@@ -11092,6 +12601,9 @@ packages:
debug:
optional: true
+ fontfaceobserver@2.3.0:
+ resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==}
+
for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
@@ -11271,6 +12783,10 @@ packages:
get-tsconfig@4.11.0:
resolution: {integrity: sha512-sNsqf7XKQ38IawiVGPOoAlqZo1DMrO7TU+ZcZwi7yLl7/7S0JwmoBMKz/IkUPhSoXM0Ng3vT0yB1iCe5XavDeQ==}
+ getenv@2.0.0:
+ resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==}
+ engines: {node: '>=6'}
+
gif.js@0.2.0:
resolution: {integrity: sha512-bYxCoT8OZKmbxY8RN4qDiYuj4nrQDTzgLRcFVovyona1PTWNePzI4nzOmotnlOFIzTk/ZxAHtv+TfVLiBWj/hw==}
@@ -11300,6 +12816,10 @@ packages:
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
+ glob@13.0.6:
+ resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
+ engines: {node: 18 || 20 || >=22}
+
glob@7.1.7:
resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
@@ -11457,12 +12977,39 @@ packages:
hastscript@9.0.1:
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
+ hermes-compiler@0.14.1:
+ resolution: {integrity: sha512-+RPPQlayoZ9n6/KXKt5SFILWXCGJ/LV5d24L5smXrvTDrPS4L6dSctPczXauuvzFP3QEJbD1YO7Z3Ra4a+4IhA==}
+
hermes-estree@0.25.1:
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
+ hermes-estree@0.32.0:
+ resolution: {integrity: sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==}
+
+ hermes-estree@0.32.1:
+ resolution: {integrity: sha512-ne5hkuDxheNBAikDjqvCZCwihnz0vVu9YsBzAEO1puiyFR4F1+PAz/SiPHSsNTuOveCYGRMX8Xbx4LOubeC0Qg==}
+
+ hermes-estree@0.33.3:
+ resolution: {integrity: sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==}
+
+ hermes-estree@0.35.0:
+ resolution: {integrity: sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==}
+
hermes-parser@0.25.1:
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+ hermes-parser@0.32.0:
+ resolution: {integrity: sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==}
+
+ hermes-parser@0.32.1:
+ resolution: {integrity: sha512-175dz634X/W5AiwrpLdoMl/MOb17poLHyIqgyExlE8D9zQ1OPnoORnGMB5ltRKnpvQzBjMYvT2rN/sHeIfZW5Q==}
+
+ hermes-parser@0.33.3:
+ resolution: {integrity: sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==}
+
+ hermes-parser@0.35.0:
+ resolution: {integrity: sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==}
+
hls.js@0.14.17:
resolution: {integrity: sha512-25A7+m6qqp6UVkuzUQ//VVh2EEOPYlOBg32ypr34bcPO7liBMOkKFvbjbCBfiPAOTA/7BSx1Dujft3Th57WyFg==}
@@ -11472,6 +13019,9 @@ packages:
hls.js@1.6.2:
resolution: {integrity: sha512-rx+pETSCJEDThm/JCm8CuadcAC410cVjb1XVXFNDKFuylaayHk1+tFxhkjvnMDAfqsJHxZXDAJ3Uc2d5xQyWlQ==}
+ hoist-non-react-statics@3.3.2:
+ resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
+
hono@4.12.12:
resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==}
engines: {node: '>=16.9.0'}
@@ -11577,6 +13127,9 @@ packages:
humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
+ hyphenate-style-name@1.1.0:
+ resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==}
+
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
@@ -11607,14 +13160,15 @@ packages:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
- ignore@7.0.4:
- resolution: {integrity: sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==}
- engines: {node: '>= 4'}
-
ignore@7.0.5:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
+ image-size@1.2.1:
+ resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==}
+ engines: {node: '>=16.x'}
+ hasBin: true
+
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
@@ -11661,6 +13215,9 @@ packages:
inline-style-parser@0.2.4:
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
+ inline-style-prefixer@7.0.1:
+ resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==}
+
inspect-with-kind@1.0.5:
resolution: {integrity: sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==}
@@ -11675,6 +13232,9 @@ packages:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
+ invariant@2.2.4:
+ resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
+
ioredis@5.6.1:
resolution: {integrity: sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==}
engines: {node: '>=12.22.0'}
@@ -11970,6 +13530,10 @@ packages:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
+ istanbul-lib-instrument@5.2.1:
+ resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==}
+ engines: {node: '>=8'}
+
istanbul-lib-report@3.0.1:
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
engines: {node: '>=10'}
@@ -12002,10 +13566,57 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ jest-diff@30.4.1:
+ resolution: {integrity: sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-environment-node@29.7.0:
+ resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ jest-get-type@29.6.3:
+ resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ jest-haste-map@29.7.0:
+ resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ jest-matcher-utils@30.4.1:
+ resolution: {integrity: sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
+ jest-message-util@29.7.0:
+ resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ jest-mock@29.7.0:
+ resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ jest-regex-util@29.6.3:
+ resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ jest-util@29.7.0:
+ resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ jest-validate@29.7.0:
+ resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
jest-worker@27.5.1:
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
engines: {node: '>= 10.13.0'}
+ jest-worker@29.7.0:
+ resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ jimp-compact@0.16.1:
+ resolution: {integrity: sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==}
+
jiti@1.21.7:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true
@@ -12059,6 +13670,9 @@ packages:
jsbn@1.1.0:
resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==}
+ jsc-safe-url@0.2.4:
+ resolution: {integrity: sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==}
+
jsdoc-type-pratt-parser@4.1.0:
resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==}
engines: {node: '>=12.0.0'}
@@ -12169,6 +13783,10 @@ packages:
engines: {node: '>=8'}
hasBin: true
+ lan-network@0.2.1:
+ resolution: {integrity: sha512-ONPnazC96VKDntab9j9JKwIWhZ4ZUceB4A9Epu4Ssg0hYFmtHZSeQ+n15nIwTFmcBUKtExOer8WTJ4GF9MO64A==}
+ hasBin: true
+
language-subtag-registry@0.3.23:
resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==}
@@ -12186,6 +13804,10 @@ packages:
leb@1.0.0:
resolution: {integrity: sha512-Y3c3QZfvKWHX60BVOQPhLCvVGmDYWyJEiINE3drOog6KCyN2AOwvuQQzlS3uJg1J85kzpILXIUwRXULWavir+w==}
+ leven@3.1.0:
+ resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
+ engines: {node: '>=6'}
+
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
@@ -12193,6 +13815,79 @@ packages:
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
+ lighthouse-logger@1.4.2:
+ resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==}
+
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
@@ -12224,6 +13919,10 @@ packages:
resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==}
engines: {node: '>=14'}
+ locate-path@5.0.0:
+ resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
+ engines: {node: '>=8'}
+
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -12265,6 +13964,9 @@ packages:
lodash.sortby@4.7.0:
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
+ lodash.throttle@4.1.1:
+ resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
+
lodash.truncate@4.4.2:
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
@@ -12274,6 +13976,10 @@ packages:
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+ log-symbols@2.2.0:
+ resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==}
+ engines: {node: '>=4'}
+
log-symbols@6.0.0:
resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==}
engines: {node: '>=18'}
@@ -12386,6 +14092,9 @@ packages:
resolution: {integrity: sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==}
engines: {node: ^16.14.0 || >=18.0.0}
+ makeerror@1.0.12:
+ resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
+
map-or-similar@1.5.0:
resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==}
@@ -12401,6 +14110,9 @@ packages:
engines: {node: '>= 16'}
hasBin: true
+ marky@1.3.0:
+ resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==}
+
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -12458,6 +14170,9 @@ packages:
mdast-util-to-string@4.0.0:
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
+ mdn-data@2.0.14:
+ resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
+
media-chrome@4.12.0:
resolution: {integrity: sha512-leItqyy2jn1nk66KGmzeH0z6JXeVUvK95O7ouQTSVdvrTXB/8jtLEI964eXlq5oCZfyPLnPUiuosKrDEfmWDqQ==}
@@ -12478,6 +14193,12 @@ packages:
mediabunny@1.45.2:
resolution: {integrity: sha512-lm34wGClgC263x8SEH5+79Z6aeDcHetoCKMSAeqDhn6Qn4a3A24Bs8uJf9Lxt9h0MEa/uJqZ/5soial/V9TSwQ==}
+ memoize-one@5.2.1:
+ resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
+
+ memoize-one@6.0.0:
+ resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
+
memoizerific@1.11.3:
resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==}
@@ -12507,6 +14228,122 @@ packages:
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
engines: {node: '>= 0.6'}
+ metro-babel-transformer@0.83.7:
+ resolution: {integrity: sha512-sBqBkt6kNut/88bv+Ucvm4yqdPetbvAEsHzi3MAgJEifOSYYzX5Z5Kgw3TFOrwf/mHJTOBG2ONlaMHoyfP15TA==}
+ engines: {node: '>=20.19.4'}
+
+ metro-babel-transformer@0.84.4:
+ resolution: {integrity: sha512-rvCfz8snl9h20VcvpOHxZuHP1SlAkv4HXbzw7nyyVwu6Eqo5PRerbakQ9XmUCOsRy70spJ37O+G1TK8oMzo48g==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+
+ metro-cache-key@0.83.7:
+ resolution: {integrity: sha512-W1c2Nmx8MiJTJt+eWhMO08z9VKi3kZOaz99IYGdqeqDgY9j+yZjXl62rUav4Di0heZfh4/n2s722PqRL1OODeg==}
+ engines: {node: '>=20.19.4'}
+
+ metro-cache-key@0.84.4:
+ resolution: {integrity: sha512-wVO79aGrkYImpnaVS4+d5RrRBRPX31QtvKB3wKGBuiNSznduZTQHzsrJZRroFJSwnygrzdsGUtDQPuqqFjFdvw==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+
+ metro-cache@0.83.7:
+ resolution: {integrity: sha512-E9SRePXQ1Zvlj79VcOk57q7VC7rMHMFQ+jhmPHBiq+dJ0bJB5BL87lWZF6oh5X76Cci5tpDuQNaDwwuSCToEeg==}
+ engines: {node: '>=20.19.4'}
+
+ metro-cache@0.84.4:
+ resolution: {integrity: sha512-gpcFQdSLUwUCk71saKoE64jLFbx2nwTfVCcPSULMNT8QYq0p1eZZE29Jvd0HtT/UlhC3ZOutLxJME5xqD2JUZg==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+
+ metro-config@0.83.7:
+ resolution: {integrity: sha512-83mjWFbFOt2GeJ6pFIum5mSnc1uTsZJAtD8o4ej0s4NVsYsA7fB+pHvTfHhFrpeMONaobu2riKavkPei05Er/Q==}
+ engines: {node: '>=20.19.4'}
+
+ metro-config@0.84.4:
+ resolution: {integrity: sha512-PMotGDjXcXLWo2TMRH+VR99phFNgYTwqh4OoieIKK3yTJa1Jmkl+fZJxDO0jfBvNF+WESHciHvpNuBtXaF3B0Q==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+
+ metro-core@0.83.7:
+ resolution: {integrity: sha512-6yn3w1wnltT6RQl7p7YES2l95ArC+mWrOssEiH8p5/DDrJS65/szf9LsC9JrBv8c5DdvSY3V3f0GRYg0Ox7hCg==}
+ engines: {node: '>=20.19.4'}
+
+ metro-core@0.84.4:
+ resolution: {integrity: sha512-HONpWC5LGXZn3ffkd4Hu6AIrfE7j4Z0g0wMo/goV24WOB3lhuFZ40KgvaDiSw8iyQHloMYay5N/wPX+z8oN/PQ==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+
+ metro-file-map@0.83.7:
+ resolution: {integrity: sha512-+j0F1m+FQYVAQ6syf+mwhIPV5GoFQrkInX8bppuc50IzNsZbMrp8R5H/Sx/K2daQ3YEa9F/XwkeZT8gzJfgeCw==}
+ engines: {node: '>=20.19.4'}
+
+ metro-file-map@0.84.4:
+ resolution: {integrity: sha512-KSVDi/u60hKPx++NLu3MTIvyjzNoJnFAF8PQFxaj1jiSka/wjw+Ua6sNuJ0TDHQv+7AAoFQxeMgaRAe8Yic5wQ==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+
+ metro-minify-terser@0.83.7:
+ resolution: {integrity: sha512-MfJar2IS4tBRuLb9svwb0Gu5l9BsH+pcRm8eGcEi/wy8MzZinfinh5dFLt2nWkocnulIgtGB5NkFDdbXqMXKhQ==}
+ engines: {node: '>=20.19.4'}
+
+ metro-minify-terser@0.84.4:
+ resolution: {integrity: sha512-5qpbaVOMC7CPitIpuewzVeGw7E+C3ykbv2mqTjQLl85Z3annSVGlSCTcsZjqXZzjupfK4Ztj3dDc4kc44NZwtQ==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+
+ metro-resolver@0.83.7:
+ resolution: {integrity: sha512-WSJIENlMcoSsuz66IfBHOkgfp3KJt2UW2TnEHPf1b8pIG2eEXNOVmo2+03A0H17WY2XGXWgxL0CG7FAopqgB1A==}
+ engines: {node: '>=20.19.4'}
+
+ metro-resolver@0.84.4:
+ resolution: {integrity: sha512-1qLgbxQ5ZGhhutuPot1Yp348ofDsATL2WkrHF65TobqTT9K3P9qJXw38bomk7ncp5B7OYMfWwtyBZo1lCV792A==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+
+ metro-runtime@0.83.7:
+ resolution: {integrity: sha512-9GKkJURaB2iyYoEExKnedzAHzxmKtSi+k0tsZUvMoU27tBZJElchYt7JH/Ai/XzYAI9lCAaV7u5HZSI8J5Z+wQ==}
+ engines: {node: '>=20.19.4'}
+
+ metro-runtime@0.84.4:
+ resolution: {integrity: sha512-Jibypds4g7AhzdRKY+kDoj51s5EXMwgyp5ddtlreDAsWefMdOx+agWqgm0H2XSZ/ueanHHVM89fnf5OJnlxa8Q==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+
+ metro-source-map@0.83.7:
+ resolution: {integrity: sha512-JgA1h7oc1a1jydBe1GhVFsUoMYo3wLPk7oRA32rjlDsq+sP2JLt9x2p2lWbNSxTm/u8NV4VRid3hvEJgcX8tKw==}
+ engines: {node: '>=20.19.4'}
+
+ metro-source-map@0.84.4:
+ resolution: {integrity: sha512-jbWkPxIesVuo1IWkvezmMJld6iu8nD62GsrZiV6jP37AOdbo4OBq1FJ+qkOg8sV05wAHB//jAbziuW0SlJfW4g==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+
+ metro-symbolicate@0.83.7:
+ resolution: {integrity: sha512-g4suyxw20WOHWI680c+Kq4wC/NF+Hx5pRH9afrMp+sMTxqLeKcPR1Xf4wMhsjlbvx7LbIREdke6q928jEjvJWw==}
+ engines: {node: '>=20.19.4'}
+ hasBin: true
+
+ metro-symbolicate@0.84.4:
+ resolution: {integrity: sha512-OnfpacxUqGPZQ27t8qK9mFa7uqHIlVWeqRqkCbvMvreEBiamEeOn8krKtcwgP5M4cYDPwuSmCTopHMVthqG4zA==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+ hasBin: true
+
+ metro-transform-plugins@0.83.7:
+ resolution: {integrity: sha512-Ss0FpBiZDjX2kwhukMDl5sNdYK8T/06IPqxNE4H6PTlRlfs9q11cef13c/xESY/Pm4VCkp1yJUZO3kXzvMxQFA==}
+ engines: {node: '>=20.19.4'}
+
+ metro-transform-plugins@0.84.4:
+ resolution: {integrity: sha512-kehr6HbAecqD0/a3xLXobELdPaAmRAl8bel0qagPF4vhZtux93nS8S4eq2kgKt6J2GnQpVjSoW1PXdst04mwow==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+
+ metro-transform-worker@0.83.7:
+ resolution: {integrity: sha512-UegCo7ygB2fT64mRK2nbAjQVJ1zSwIIHy8d96jJv2nKZFDaViYBiughEdu5HM/Ceq0WN3LZrZk3zhl9aoiLYFw==}
+ engines: {node: '>=20.19.4'}
+
+ metro-transform-worker@0.84.4:
+ resolution: {integrity: sha512-W1IYMvvXTu4MxYr7d9h7CeG2vpIr3bmLLIavkPY4O1ilzDrvS8z/NEe6y+pC44Ff7raMXQgYSfdqDUwN/i39gg==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+
+ metro@0.83.7:
+ resolution: {integrity: sha512-SPaPEyvTsTmd0LpT7RaZciQyDw2i/JB7+iY9L5VfBo72+psescFxBqpI1TL9dnL+pmnfkU+l/J1mEEGLeF65EQ==}
+ engines: {node: '>=20.19.4'}
+ hasBin: true
+
+ metro@0.84.4:
+ resolution: {integrity: sha512-8ETTubqfD6ornDy2zYDvRcKnVDOXdFJsjetYDBsY4oAsb6NJkiwFR+FaMESyGppFmQUyBQA4H4sFGxzcQSGtFA==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+ hasBin: true
+
micro-api-client@3.3.0:
resolution: {integrity: sha512-y0y6CUB9RLVsy3kfgayU28746QrNMpSm9O/AYGNsBgOkJr/X/Jk0VLGoO8Ude7Bpa8adywzF+MzXNZRFRsNPhg==}
@@ -12650,6 +14487,10 @@ packages:
engines: {node: '>=16'}
hasBin: true
+ mimic-fn@1.2.0:
+ resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==}
+ engines: {node: '>=4'}
+
mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
@@ -12742,6 +14583,10 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
+ minipass@7.1.3:
+ resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
minizlib@2.1.2:
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
engines: {node: '>= 8'}
@@ -12841,6 +14686,9 @@ packages:
multipasta@0.2.7:
resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==}
+ multitars@1.0.0:
+ resolution: {integrity: sha512-H/J4fMLedtudftaYMOg7ajzLYgT3/rwbWVJbqr/iUgB8DQztn38ys5HOqI1CzSxx8QhXXwOOnnBvd4v3jG5+Mg==}
+
mustache@4.2.0:
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
hasBin: true
@@ -13010,6 +14858,10 @@ packages:
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
engines: {node: '>= 6.13.0'}
+ node-forge@1.4.0:
+ resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==}
+ engines: {node: '>= 6.13.0'}
+
node-gyp-build-optional-packages@5.1.1:
resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==}
hasBin: true
@@ -13039,6 +14891,9 @@ packages:
node-releases@2.0.23:
resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==}
+ node-releases@2.0.44:
+ resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==}
+
node-source-walk@6.0.2:
resolution: {integrity: sha512-jn9vOIK/nfqoFCcpK89/VCVaLg1IHE6UVfDOzvqmANaJ/rWCTEdH8RZ1V278nv2jr36BJdyQXIAavBLXpzdlag==}
engines: {node: '>=14'}
@@ -13126,6 +14981,12 @@ packages:
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
deprecated: This package is no longer supported.
+ nth-check@2.1.1:
+ resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+
+ nullthrows@1.1.1:
+ resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==}
+
number-flow@0.5.7:
resolution: {integrity: sha512-P83Y9rBgN3Xpz5677YDNtuQHZpIldw6WXeWRg0+edrfFthhV7QqRdABas5gtu07QPLvbA8XhfO69rIvbKRzYIg==}
@@ -13140,6 +15001,14 @@ packages:
oauth@0.9.15:
resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==}
+ ob1@0.83.7:
+ resolution: {integrity: sha512-9M5kpuOLyTPogMtZiQUIxdAZxl7Dxs6tVBbJErSumsqGMuhVSoUbkfeZ3XNPpLpwBBtqY5QDUzGwggLHX3slQg==}
+ engines: {node: '>=20.19.4'}
+
+ ob1@0.84.4:
+ resolution: {integrity: sha512-eJXMpz4aQHXF/YBB9ddqZDIS+ooO91hObo9FoW/xBkr54/zCwYYCDqT/O54vNo8kOkWs5Ou/y28NgdrV0edQNA==}
+ engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
+
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -13205,16 +15074,28 @@ packages:
resolution: {integrity: sha512-D7EmwxJV6DsEB6vOFLrBM2OzsVgQzgPWyHlV2OOAVj772n+WTXpudC9e9u5BVKQnYwaD30Ivhi9b+4UeBcGu9g==}
engines: {node: ^10.13.0 || >=12.0.0}
+ on-finished@2.3.0:
+ resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
+ engines: {node: '>= 0.8'}
+
on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
+ on-headers@1.1.0:
+ resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==}
+ engines: {node: '>= 0.8'}
+
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
one-time@1.0.0:
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
+ onetime@2.0.1:
+ resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==}
+ engines: {node: '>=4'}
+
onetime@5.1.2:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
@@ -13240,6 +15121,10 @@ packages:
resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
engines: {node: '>=18'}
+ open@7.4.2:
+ resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
+ engines: {node: '>=8'}
+
open@8.4.0:
resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==}
engines: {node: '>=12'}
@@ -13262,6 +15147,10 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
+ ora@3.4.0:
+ resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==}
+ engines: {node: '>=6'}
+
ora@8.2.0:
resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==}
engines: {node: '>=18'}
@@ -13286,6 +15175,10 @@ packages:
resolution: {integrity: sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ p-limit@2.3.0:
+ resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
+ engines: {node: '>=6'}
+
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
@@ -13294,6 +15187,10 @@ packages:
resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ p-locate@4.1.0:
+ resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
+ engines: {node: '>=8'}
+
p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
@@ -13318,6 +15215,10 @@ packages:
resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==}
engines: {node: '>=14.16'}
+ p-try@2.2.0:
+ resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
+ engines: {node: '>=6'}
+
p-wait-for@5.0.2:
resolution: {integrity: sha512-lwx6u1CotQYPVju77R+D0vFomni/AqRfqLmqQ8hekklqZ6gAY9rONh7lBQ0uxWMkC2AuX9b2DVAl8To0NyP1JA==}
engines: {node: '>=12'}
@@ -13368,6 +15269,10 @@ packages:
parse-numeric-range@1.3.0:
resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==}
+ parse-png@2.1.0:
+ resolution: {integrity: sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==}
+ engines: {node: '>=10'}
+
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
@@ -13413,6 +15318,10 @@ packages:
resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
engines: {node: 20 || >=22}
+ path-scurry@2.0.2:
+ resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
+ engines: {node: 18 || 20 || >=22}
+
path-to-regexp@0.1.13:
resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==}
@@ -13502,10 +15411,18 @@ packages:
player.style@0.1.8:
resolution: {integrity: sha512-r/k2gX1IS3tIH/MQJsXQ0pt54s0NqQ70jSePQSKBlu1Rwf/qqdFUmSKACL10ebS48B0VioclwbvKzBnqgb52Tw==}
+ plist@3.1.1:
+ resolution: {integrity: sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==}
+ engines: {node: '>=10.4.0'}
+
pluralize@8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
+ pngjs@3.4.0:
+ resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==}
+ engines: {node: '>=4.0.0'}
+
polished@4.3.1:
resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==}
engines: {node: '>=10'}
@@ -13589,6 +15506,10 @@ packages:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
+ postcss@8.4.49:
+ resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
+ engines: {node: ^10 || ^12 || >=14}
+
postcss@8.5.3:
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
engines: {node: ^10 || ^12 || >=14}
@@ -13642,9 +15563,17 @@ packages:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+ pretty-format@29.7.0:
+ resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
pretty-format@3.8.0:
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
+ pretty-format@30.4.1:
+ resolution: {integrity: sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==}
+ engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+
printable-characters@1.0.42:
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
@@ -13689,6 +15618,12 @@ packages:
resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
engines: {node: '>=10'}
+ promise@7.3.1:
+ resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
+
+ promise@8.3.0:
+ resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==}
+
prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
@@ -13743,6 +15678,10 @@ packages:
quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
+ query-string@7.1.3:
+ resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==}
+ engines: {node: '>=6'}
+
querystring@0.2.0:
resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==}
engines: {node: '>=0.4.x'}
@@ -13751,6 +15690,9 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+ queue@6.0.2:
+ resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}
+
quick-lru@5.1.1:
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
engines: {node: '>=10'}
@@ -13794,11 +15736,19 @@ packages:
peerDependencies:
react: ^16.3.0 || ^17.0.1 || ^18.0.0 || ^19.0.0
+ react-devtools-core@6.1.5:
+ resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==}
+
react-dom@19.1.1:
resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==}
peerDependencies:
react: ^19.1.1
+ react-dom@19.2.0:
+ resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==}
+ peerDependencies:
+ react: ^19.2.0
+
react-dom@19.2.4:
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
peerDependencies:
@@ -13820,6 +15770,15 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
+ react-fast-compare@3.2.2:
+ resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
+
+ react-freeze@1.0.4:
+ resolution: {integrity: sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ react: '>=17.0.0'
+
react-hls-player@3.0.7:
resolution: {integrity: sha512-i5QWNyLmaUhV/mgnpljRJT0CBfJnylClV/bne8aiXO3ZqU0+D3U/jtTDwdXM4i5qHhyFy9lemyZ179IgadKd0Q==}
peerDependencies:
@@ -13854,6 +15813,12 @@ packages:
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+ react-is@18.3.1:
+ resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
+
+ react-is@19.2.6:
+ resolution: {integrity: sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==}
+
react-loading-skeleton@3.5.0:
resolution: {integrity: sha512-gxxSyLbrEAdXTKgfbpBEFZCO/P153DnqSCQau2+o6lNy1jgMRr2MmRmOzMmyrwSaSYLRB8g7b0waYPmUjz7IhQ==}
peerDependencies:
@@ -13865,6 +15830,73 @@ packages:
'@types/react': '>=18'
react: '>=18'
+ react-native-gesture-handler@2.30.1:
+ resolution: {integrity: sha512-xIUBDo5ktmJs++0fZlavQNvDEE4PsihWhSeJsJtoz4Q6p0MiTM9TgrTgfEgzRR36qGPytFoeq+ShLrVwGdpUdA==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
+ react-native-is-edge-to-edge@1.2.1:
+ resolution: {integrity: sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
+ react-native-is-edge-to-edge@1.3.1:
+ resolution: {integrity: sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
+ react-native-reanimated@4.2.1:
+ resolution: {integrity: sha512-/NcHnZMyOvsD/wYXug/YqSKw90P9edN0kEPL5lP4PFf1aQ4F1V7MKe/E0tvfkXKIajy3Qocp5EiEnlcrK/+BZg==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+ react-native-worklets: 0.7.4
+
+ react-native-safe-area-context@5.6.2:
+ resolution: {integrity: sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
+ react-native-screens@4.23.0:
+ resolution: {integrity: sha512-XhO3aK0UeLpBn4kLecd+J+EDeRRJlI/Ro9Fze06vo1q163VeYtzfU9QS09/VyDFMWR1qxDC1iazCArTPSFFiPw==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
+ react-native-svg@15.15.5:
+ resolution: {integrity: sha512-L4go5jA+GWutdJ/JucuN20cjAbMg1HmMtAP+wZ+3JLCf6Jd0bhXQHxciRP/AQm/FlrIEZwkMcHNZP+FXAiic0w==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
+ react-native-web@0.21.2:
+ resolution: {integrity: sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==}
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+
+ react-native-worklets@0.7.4:
+ resolution: {integrity: sha512-NYOdM1MwBb3n+AtMqy1tFy3Mn8DliQtd8sbzAVRf9Gc+uvQ0zRfxN7dS8ZzoyX7t6cyQL5THuGhlnX+iFlQTag==}
+ peerDependencies:
+ '@babel/core': '*'
+ react: '*'
+ react-native: '*'
+
+ react-native@0.83.6:
+ resolution: {integrity: sha512-H513+8VzviNFXOdPnStRzX9S3/jiJGg++QZ1zd+ROyAvBEKqFqKUPHH0d82y3QyRPct5qKjdOa7J6vNehCvXYA==}
+ engines: {node: '>= 20.19.4'}
+ hasBin: true
+ peerDependencies:
+ '@types/react': ^19.1.1
+ react: ^19.2.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
react-promise-suspense@0.3.4:
resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==}
@@ -13880,6 +15912,10 @@ packages:
redux:
optional: true
+ react-refresh@0.14.2:
+ resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
+ engines: {node: '>=0.10.0'}
+
react-refresh@0.17.0:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'}
@@ -13950,6 +15986,11 @@ packages:
'@types/react':
optional: true
+ react-test-renderer@19.2.0:
+ resolution: {integrity: sha512-zLCFMHFE9vy/w3AxO0zNxy6aAupnCuLSVOJYDe/Tp+ayGI1f2PLQsFVPANSD42gdSbmYx5oN+1VWDhcXtq7hAQ==}
+ peerDependencies:
+ react: ^19.2.0
+
react-tooltip@5.28.1:
resolution: {integrity: sha512-ZA4oHwoIIK09TS7PvSLFcRlje1wGZaxw6xHvfrzn6T82UcMEfEmHVCad16Gnr4NDNDh93HyN037VK4HDi5odfQ==}
peerDependencies:
@@ -13960,6 +16001,10 @@ packages:
resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
engines: {node: '>=0.10.0'}
+ react@19.2.0:
+ resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
+ engines: {node: '>=0.10.0'}
+
react@19.2.4:
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'}
@@ -14060,6 +16105,16 @@ packages:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'}
+ regenerate-unicode-properties@10.2.2:
+ resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==}
+ engines: {node: '>=4'}
+
+ regenerate@1.4.2:
+ resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
+
+ regenerator-runtime@0.13.11:
+ resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
+
regex-recursion@5.1.1:
resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==}
@@ -14083,6 +16138,17 @@ packages:
resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==}
engines: {node: '>=8'}
+ regexpu-core@6.4.0:
+ resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==}
+ engines: {node: '>=4'}
+
+ regjsgen@0.8.0:
+ resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==}
+
+ regjsparser@0.13.1:
+ resolution: {integrity: sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==}
+ hasBin: true
+
rehype-parse@9.0.1:
resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==}
@@ -14156,11 +16222,19 @@ packages:
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+ resolve-workspace-root@2.0.1:
+ resolution: {integrity: sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w==}
+
resolve@1.22.10:
resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
engines: {node: '>= 0.4'}
hasBin: true
+ resolve@1.22.12:
+ resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==}
+ engines: {node: '>= 0.4'}
+ hasBin: true
+
resolve@2.0.0-next.5:
resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
hasBin: true
@@ -14172,6 +16246,10 @@ packages:
resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==}
engines: {node: '>=14.16'}
+ restore-cursor@2.0.0:
+ resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==}
+ engines: {node: '>=4'}
+
restore-cursor@5.1.0:
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
engines: {node: '>=18'}
@@ -14333,6 +16411,11 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
+ semver@7.6.3:
+ resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
+ engines: {node: '>=10'}
+ hasBin: true
+
semver@7.7.1:
resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
engines: {node: '>=10'}
@@ -14364,6 +16447,10 @@ packages:
seq-queue@0.0.5:
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
+ serialize-error@2.1.0:
+ resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==}
+ engines: {node: '>=0.10.0'}
+
serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
@@ -14412,6 +16499,13 @@ packages:
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
+ sf-symbols-typescript@2.2.0:
+ resolution: {integrity: sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==}
+ engines: {node: '>=10'}
+
+ shallowequal@1.1.0:
+ resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
+
sharp@0.33.5:
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -14471,6 +16565,9 @@ packages:
resolution: {integrity: sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==}
engines: {node: ^16.14.0 || >=18.0.0}
+ simple-plist@1.3.1:
+ resolution: {integrity: sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==}
+
simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
@@ -14493,6 +16590,10 @@ packages:
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
engines: {node: '>=10'}
+ slugify@1.6.9:
+ resolution: {integrity: sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==}
+ engines: {node: '>=8.0.0'}
+
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
@@ -14591,6 +16692,10 @@ packages:
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
+ source-map@0.5.7:
+ resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
+ engines: {node: '>=0.10.0'}
+
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
@@ -14623,6 +16728,10 @@ packages:
spdx-license-ids@3.0.21:
resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==}
+ split-on-first@1.1.0:
+ resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
+ engines: {node: '>=6'}
+
sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
@@ -14687,18 +16796,30 @@ packages:
stack-trace@0.0.10:
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
+ stack-utils@2.0.6:
+ resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
+ engines: {node: '>=10'}
+
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
stackframe@1.3.4:
resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==}
+ stacktrace-parser@0.1.11:
+ resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==}
+ engines: {node: '>=6'}
+
stacktracey@2.1.8:
resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==}
standard-as-callback@2.1.0:
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
+ statuses@1.5.0:
+ resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
+ engines: {node: '>= 0.6'}
+
statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
@@ -14752,9 +16873,17 @@ packages:
prettier:
optional: true
+ stream-buffers@2.2.0:
+ resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==}
+ engines: {node: '>= 0.10.0'}
+
streamx@2.22.0:
resolution: {integrity: sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==}
+ strict-uri-encode@2.0.0:
+ resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
+ engines: {node: '>=4'}
+
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@@ -14799,6 +16928,10 @@ packages:
stringify-entities@4.0.4:
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
+ strip-ansi@5.2.0:
+ resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==}
+ engines: {node: '>=6'}
+
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@@ -14853,6 +16986,9 @@ packages:
resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==}
engines: {node: '>=18'}
+ structured-headers@0.4.1:
+ resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==}
+
style-to-js@1.1.16:
resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==}
@@ -14875,6 +17011,9 @@ packages:
babel-plugin-macros:
optional: true
+ styleq@0.1.3:
+ resolution: {integrity: sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==}
+
subtitles-parser-vtt@0.1.0:
resolution: {integrity: sha512-+y3GOvLL+71JLMFFjqSi4p0J9ddSbhpXKaWG6vHUT8PqPZmlhyAsfu0LP248FdVGfwNIj77wIgVkfQ2xwCZ4+Q==}
@@ -14902,6 +17041,10 @@ packages:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'}
+ supports-hyperlinks@2.3.0:
+ resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==}
+ engines: {node: '>=8'}
+
supports-hyperlinks@4.4.0:
resolution: {integrity: sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==}
engines: {node: '>=20'}
@@ -14967,6 +17110,10 @@ packages:
tauri-plugin-positioner-api@0.2.7:
resolution: {integrity: sha512-jwqRHo59UU3aJbffEFkWVhBorjQg1WNeDa4W4eWVnaTqLals+/fqgHdNwTGzG1+LLdaJSS2FUy4XSwEDAWvERQ==}
+ terminal-link@2.1.1:
+ resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==}
+ engines: {node: '>=8'}
+
terminal-link@5.0.0:
resolution: {integrity: sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA==}
engines: {node: '>=20'}
@@ -15003,6 +17150,10 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ test-exclude@6.0.0:
+ resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
+ engines: {node: '>=8'}
+
test-exclude@7.0.1:
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
engines: {node: '>=18'}
@@ -15023,6 +17174,9 @@ packages:
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
+ throat@5.0.0:
+ resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==}
+
through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
@@ -15098,6 +17252,9 @@ packages:
resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
engines: {node: '>=14.14'}
+ tmpl@1.0.5:
+ resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
+
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -15117,6 +17274,9 @@ packages:
toml@3.0.0:
resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==}
+ toqr@0.1.1:
+ resolution: {integrity: sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA==}
+
totalist@3.0.1:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
@@ -15305,6 +17465,10 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
+ type-detect@4.0.8:
+ resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
+ engines: {node: '>=4'}
+
type-fest@0.20.2:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'}
@@ -15313,6 +17477,10 @@ packages:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
+ type-fest@0.7.1:
+ resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==}
+ engines: {node: '>=8'}
+
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
@@ -15356,6 +17524,11 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
ua-parser-js@1.0.41:
resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==}
hasBin: true
@@ -15440,6 +17613,22 @@ packages:
unenv@2.0.0-rc.15:
resolution: {integrity: sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==}
+ unicode-canonical-property-names-ecmascript@2.0.1:
+ resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
+ engines: {node: '>=4'}
+
+ unicode-match-property-ecmascript@2.0.0:
+ resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==}
+ engines: {node: '>=4'}
+
+ unicode-match-property-value-ecmascript@2.2.1:
+ resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==}
+ engines: {node: '>=4'}
+
+ unicode-property-aliases-ecmascript@2.2.0:
+ resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==}
+ engines: {node: '>=4'}
+
unicorn-magic@0.1.0:
resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==}
engines: {node: '>=18'}
@@ -15671,6 +17860,12 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
uqr@0.1.2:
resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==}
@@ -15704,6 +17899,11 @@ packages:
peerDependencies:
react: '>=16.8.0'
+ use-latest-callback@0.2.6:
+ resolution: {integrity: sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==}
+ peerDependencies:
+ react: '>=16.8'
+
use-resize-observer@9.1.0:
resolution: {integrity: sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==}
peerDependencies:
@@ -15744,6 +17944,11 @@ packages:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
+ uuid@7.0.3:
+ resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==}
+ deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
+ hasBin: true
+
uuid@8.0.0:
resolution: {integrity: sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==}
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
@@ -15787,6 +17992,12 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
+ vaul@1.1.2:
+ resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==}
+ peerDependencies:
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
+
vfile-location@5.0.3:
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
@@ -16024,6 +18235,9 @@ packages:
jsdom:
optional: true
+ vlq@1.0.1:
+ resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==}
+
w3c-xmlserializer@5.0.0:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
@@ -16031,6 +18245,12 @@ packages:
walk-up-path@3.0.1:
resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==}
+ walker@1.0.8:
+ resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
+
+ warn-once@0.1.1:
+ resolution: {integrity: sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==}
+
watchpack@2.5.1:
resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==}
engines: {node: '>=10.13.0'}
@@ -16113,10 +18333,16 @@ packages:
engines: {node: '>=18'}
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
+ whatwg-fetch@3.6.20:
+ resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==}
+
whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
+ whatwg-url-minimum@0.1.2:
+ resolution: {integrity: sha512-XPEm0XFQWNVG292lII1PrRRJl3sItrs7CettZ4ncYxuDVpLyy+NwlGyut2hXI0JswcJUxeCH+CyOJK0ZzAXD6A==}
+
whatwg-url@14.2.0:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
@@ -16238,6 +18464,10 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ write-file-atomic@4.0.2:
+ resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==}
+ engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
+
write-file-atomic@5.0.1:
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -16246,6 +18476,18 @@ packages:
resolution: {integrity: sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==}
engines: {node: ^18.17.0 || >=20.5.0}
+ ws@7.5.10:
+ resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==}
+ engines: {node: '>=8.3.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: ^5.0.2
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
ws@8.17.1:
resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
engines: {node: '>=10.0.0'}
@@ -16298,6 +18540,10 @@ packages:
resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==}
engines: {node: '>=18'}
+ xcode@3.0.1:
+ resolution: {integrity: sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==}
+ engines: {node: '>=10.0.0'}
+
xdg-app-paths@5.1.0:
resolution: {integrity: sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA==}
engines: {node: '>=6'}
@@ -16310,6 +18556,10 @@ packages:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'}
+ xml2js@0.6.0:
+ resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==}
+ engines: {node: '>=4.0.0'}
+
xml2js@0.6.2:
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
engines: {node: '>=4.0.0'}
@@ -16318,6 +18568,10 @@ packages:
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
engines: {node: '>=4.0'}
+ xmlbuilder@15.1.1:
+ resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==}
+ engines: {node: '>=8.0'}
+
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
@@ -17588,8 +19842,16 @@ snapshots:
js-tokens: 4.0.0
picocolors: 1.1.1
+ '@babel/code-frame@7.29.0':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.28.5
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
'@babel/compat-data@7.27.2': {}
+ '@babel/compat-data@7.29.3': {}
+
'@babel/core@7.27.1':
dependencies:
'@ampproject/remapping': 2.3.0
@@ -17610,15 +19872,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@babel/generator@7.27.1':
- dependencies:
- '@babel/parser': 7.27.5
- '@babel/types': 7.27.1
- '@jridgewell/gen-mapping': 0.3.13
- '@jridgewell/trace-mapping': 0.3.31
- jsesc: 3.1.0
-
- '@babel/generator@7.27.5':
+ '@babel/generator@7.28.3':
dependencies:
'@babel/parser': 7.28.4
'@babel/types': 7.28.4
@@ -17626,14 +19880,18 @@ snapshots:
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
- '@babel/generator@7.28.3':
+ '@babel/generator@7.29.1':
dependencies:
- '@babel/parser': 7.28.4
- '@babel/types': 7.28.4
+ '@babel/parser': 7.29.3
+ '@babel/types': 7.29.0
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
+ '@babel/helper-annotate-as-pure@7.27.3':
+ dependencies:
+ '@babel/types': 7.29.0
+
'@babel/helper-compilation-targets@7.27.2':
dependencies:
'@babel/compat-data': 7.27.2
@@ -17642,36 +19900,132 @@ snapshots:
lru-cache: 5.1.1
semver: 6.3.1
+ '@babel/helper-compilation-targets@7.28.6':
+ dependencies:
+ '@babel/compat-data': 7.29.3
+ '@babel/helper-validator-option': 7.27.1
+ browserslist: 4.28.2
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-create-class-features-plugin@7.29.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-member-expression-to-functions': 7.28.5
+ '@babel/helper-optimise-call-expression': 7.27.1
+ '@babel/helper-replace-supers': 7.28.6(@babel/core@7.27.1)
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ '@babel/traverse': 7.29.0
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.27.3
+ regexpu-core: 6.4.0
+ semver: 6.3.1
+
+ '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ debug: 4.4.3(supports-color@8.1.1)
+ lodash.debounce: 4.0.8
+ resolve: 1.22.12
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/helper-globals@7.28.0': {}
+ '@babel/helper-member-expression-to-functions@7.28.5':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/helper-module-imports@7.18.6':
dependencies:
- '@babel/types': 7.27.6
+ '@babel/types': 7.29.0
- '@babel/helper-module-imports@7.27.1':
+ '@babel/helper-module-imports@7.28.6':
dependencies:
- '@babel/traverse': 7.28.4
- '@babel/types': 7.28.4
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
transitivePeerDependencies:
- supports-color
'@babel/helper-module-transforms@7.27.1(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
- '@babel/helper-module-imports': 7.27.1
+ '@babel/helper-module-imports': 7.28.6
'@babel/helper-validator-identifier': 7.27.1
'@babel/traverse': 7.28.4
transitivePeerDependencies:
- supports-color
+ '@babel/helper-module-transforms@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-validator-identifier': 7.28.5
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-optimise-call-expression@7.27.1':
+ dependencies:
+ '@babel/types': 7.29.0
+
'@babel/helper-plugin-utils@7.27.1': {}
+ '@babel/helper-plugin-utils@7.28.6': {}
+
+ '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-wrap-function': 7.28.6
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-replace-supers@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-member-expression-to-functions': 7.28.5
+ '@babel/helper-optimise-call-expression': 7.27.1
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.27.1':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.27.1': {}
+ '@babel/helper-validator-identifier@7.28.5': {}
+
'@babel/helper-validator-option@7.27.1': {}
+ '@babel/helper-wrap-function@7.28.6':
+ dependencies:
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/helpers@7.27.1':
dependencies:
'@babel/template': 7.27.2
@@ -17686,7 +20040,7 @@ snapshots:
'@babel/parser@7.27.2':
dependencies:
- '@babel/types': 7.27.1
+ '@babel/types': 7.28.4
'@babel/parser@7.27.5':
dependencies:
@@ -17696,16 +20050,362 @@ snapshots:
dependencies:
'@babel/types': 7.28.4
+ '@babel/parser@7.29.3':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/plugin-proposal-decorators@7.29.0(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-proposal-export-default-from@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-export-default-from@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-flow@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
'@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
'@babel/helper-plugin-utils': 7.27.1
+ '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
'@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
'@babel/helper-plugin-utils': 7.27.1
+ '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.27.1)
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-classes@7.28.4(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-globals': 7.28.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-replace-supers': 7.28.6(@babel/core@7.27.1)
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-classes@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-globals': 7.28.0
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-replace-supers': 7.28.6(@babel/core@7.27.1)
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/template': 7.28.6
+
+ '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.27.1)
+
+ '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-literals@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-compilation-targets': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.27.1)
+ '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.27.1)
+ '@babel/traverse': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.28.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.1)':
dependencies:
'@babel/core': 7.27.1
@@ -17716,6 +20416,114 @@ snapshots:
'@babel/core': 7.27.1
'@babel/helper-plugin-utils': 7.27.1
+ '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.27.1)
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-runtime@7.29.0(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.27.1)
+ babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.27.1)
+ babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.27.1)
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-spread@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-skip-transparent-expression-wrappers': 7.27.1
+ '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.27.1)
+ '@babel/helper-plugin-utils': 7.28.6
+
+ '@babel/preset-react@7.28.5(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-validator-option': 7.27.1
+ '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/preset-typescript@7.27.1(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-validator-option': 7.27.1
+ '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/preset-typescript@7.28.5(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/helper-validator-option': 7.27.1
+ '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/runtime@7.27.1': {}
'@babel/standalone@7.27.2': {}
@@ -17723,16 +20531,22 @@ snapshots:
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
- '@babel/parser': 7.27.5
- '@babel/types': 7.27.1
+ '@babel/parser': 7.28.4
+ '@babel/types': 7.28.4
+
+ '@babel/template@7.28.6':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/parser': 7.29.3
+ '@babel/types': 7.29.0
'@babel/traverse@7.27.1':
dependencies:
'@babel/code-frame': 7.27.1
- '@babel/generator': 7.27.1
- '@babel/parser': 7.27.5
- '@babel/template': 7.27.2
- '@babel/types': 7.27.1
+ '@babel/generator': 7.29.1
+ '@babel/parser': 7.29.3
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
debug: 4.4.3(supports-color@8.1.1)
globals: 11.12.0
transitivePeerDependencies:
@@ -17741,7 +20555,7 @@ snapshots:
'@babel/traverse@7.27.4':
dependencies:
'@babel/code-frame': 7.27.1
- '@babel/generator': 7.27.5
+ '@babel/generator': 7.29.1
'@babel/parser': 7.27.5
'@babel/template': 7.27.2
'@babel/types': 7.27.6
@@ -17753,7 +20567,7 @@ snapshots:
'@babel/traverse@7.28.4':
dependencies:
'@babel/code-frame': 7.27.1
- '@babel/generator': 7.28.3
+ '@babel/generator': 7.29.1
'@babel/helper-globals': 7.28.0
'@babel/parser': 7.28.4
'@babel/template': 7.27.2
@@ -17762,6 +20576,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@babel/traverse@7.29.0':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/generator': 7.29.1
+ '@babel/helper-globals': 7.28.0
+ '@babel/parser': 7.29.3
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+ debug: 4.4.3(supports-color@8.1.1)
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/types@7.27.1':
dependencies:
'@babel/helper-string-parser': 7.27.1
@@ -17777,6 +20603,11 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
+ '@babel/types@7.29.0':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+
'@bcoe/v8-coverage@1.0.2': {}
'@biomejs/biome@2.2.0':
@@ -17871,7 +20702,7 @@ snapshots:
optionalDependencies:
workerd: 1.20250408.0
- '@cloudflare/vitest-pool-workers@0.6.16(@cloudflare/workers-types@4.20250507.0)(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0))':
+ '@cloudflare/vitest-pool-workers@0.6.16(@cloudflare/workers-types@4.20250507.0)(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.44.0))':
dependencies:
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
@@ -17881,7 +20712,7 @@ snapshots:
esbuild: 0.17.19
miniflare: 3.20250204.1
semver: 7.7.1
- vitest: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0)
+ vitest: 2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.44.0)
wrangler: 3.109.1(@cloudflare/workers-types@4.20250507.0)
zod: 3.25.76
transitivePeerDependencies:
@@ -18131,6 +20962,10 @@ snapshots:
'@effect/rpc': 0.71.0(@effect/platform@0.92.1(effect@3.18.4))(effect@3.18.4)
effect: 3.18.4
+ '@egjs/hammerjs@2.0.17':
+ dependencies:
+ '@types/hammerjs': 2.0.46
+
'@emnapi/core@1.10.0':
dependencies:
'@emnapi/wasi-threads': 1.2.1
@@ -18143,12 +20978,6 @@ snapshots:
tslib: 2.8.1
optional: true
- '@emnapi/core@1.9.1':
- dependencies:
- '@emnapi/wasi-threads': 1.2.0
- tslib: 2.8.1
- optional: true
-
'@emnapi/runtime@1.10.0':
dependencies:
tslib: 2.8.1
@@ -18169,21 +20998,11 @@ snapshots:
tslib: 2.8.1
optional: true
- '@emnapi/runtime@1.9.1':
- dependencies:
- tslib: 2.8.1
- optional: true
-
'@emnapi/wasi-threads@1.1.0':
dependencies:
tslib: 2.8.1
optional: true
- '@emnapi/wasi-threads@1.2.0':
- dependencies:
- tslib: 2.8.1
- optional: true
-
'@emnapi/wasi-threads@1.2.1':
dependencies:
tslib: 2.8.1
@@ -18881,6 +21700,353 @@ snapshots:
'@eslint/core': 0.15.1
levn: 0.4.1
+ '@expo-google-fonts/material-symbols@0.4.38': {}
+
+ '@expo/cli@55.0.30(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-constants@55.0.16)(expo-font@55.0.7)(expo-router@55.0.14)(expo@55.0.24)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)':
+ dependencies:
+ '@expo/code-signing-certificates': 0.0.6
+ '@expo/config': 55.0.17(typescript@5.9.3)
+ '@expo/config-plugins': 55.0.9
+ '@expo/devcert': 1.2.1
+ '@expo/env': 2.1.2
+ '@expo/image-utils': 0.8.14(typescript@5.9.3)
+ '@expo/json-file': 10.0.14
+ '@expo/log-box': 55.0.12(@expo/dom-webview@55.0.6)(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@expo/metro': 55.1.1
+ '@expo/metro-config': 55.0.21(expo@55.0.24)(typescript@5.9.3)
+ '@expo/osascript': 2.4.3
+ '@expo/package-manager': 1.10.5
+ '@expo/plist': 0.5.3
+ '@expo/prebuild-config': 55.0.18(expo@55.0.24)(typescript@5.9.3)
+ '@expo/require-utils': 55.0.5(typescript@5.9.3)
+ '@expo/router-server': 55.0.16(@expo/metro-runtime@55.0.11)(expo-constants@55.0.16)(expo-font@55.0.7)(expo-router@55.0.14)(expo-server@55.0.9)(expo@55.0.24)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@expo/schema-utils': 55.0.4
+ '@expo/spawn-async': 1.7.2
+ '@expo/ws-tunnel': 1.0.6
+ '@expo/xcpretty': 4.4.4
+ '@react-native/dev-middleware': 0.83.6
+ accepts: 1.3.8
+ arg: 5.0.2
+ better-opn: 3.0.2
+ bplist-creator: 0.1.0
+ bplist-parser: 0.3.2
+ chalk: 4.1.2
+ ci-info: 3.9.0
+ compression: 1.8.1
+ connect: 3.7.0
+ debug: 4.4.3(supports-color@8.1.1)
+ dnssd-advertise: 1.1.4
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-server: 55.0.9
+ fetch-nodeshim: 0.4.10
+ getenv: 2.0.0
+ glob: 13.0.6
+ lan-network: 0.2.1
+ multitars: 1.0.0
+ node-forge: 1.4.0
+ npm-package-arg: 11.0.3
+ ora: 3.4.0
+ picomatch: 4.0.3
+ pretty-format: 29.7.0
+ progress: 2.0.3
+ prompts: 2.4.2
+ resolve-from: 5.0.0
+ semver: 7.7.4
+ send: 0.19.0
+ slugify: 1.6.9
+ source-map-support: 0.5.21
+ stacktrace-parser: 0.1.11
+ structured-headers: 0.4.1
+ terminal-link: 2.1.1
+ toqr: 0.1.1
+ wrap-ansi: 7.0.0
+ ws: 8.18.3
+ zod: 3.25.76
+ optionalDependencies:
+ expo-router: 55.0.14(eed5efedde241c317111b390e3f7dd2b)
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ transitivePeerDependencies:
+ - '@expo/dom-webview'
+ - '@expo/metro-runtime'
+ - bufferutil
+ - expo-constants
+ - expo-font
+ - react
+ - react-dom
+ - react-server-dom-webpack
+ - supports-color
+ - typescript
+ - utf-8-validate
+
+ '@expo/code-signing-certificates@0.0.6':
+ dependencies:
+ node-forge: 1.4.0
+
+ '@expo/config-plugins@55.0.9':
+ dependencies:
+ '@expo/config-types': 55.0.5
+ '@expo/json-file': 10.0.14
+ '@expo/plist': 0.5.3
+ '@expo/sdk-runtime-versions': 1.0.0
+ chalk: 4.1.2
+ debug: 4.4.3(supports-color@8.1.1)
+ getenv: 2.0.0
+ glob: 13.0.6
+ resolve-from: 5.0.0
+ semver: 7.7.4
+ slugify: 1.6.9
+ xcode: 3.0.1
+ xml2js: 0.6.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@expo/config-types@55.0.5': {}
+
+ '@expo/config@55.0.17(typescript@5.9.3)':
+ dependencies:
+ '@expo/config-plugins': 55.0.9
+ '@expo/config-types': 55.0.5
+ '@expo/json-file': 10.0.14
+ '@expo/require-utils': 55.0.5(typescript@5.9.3)
+ deepmerge: 4.3.1
+ getenv: 2.0.0
+ glob: 13.0.6
+ resolve-workspace-root: 2.0.1
+ semver: 7.7.4
+ slugify: 1.6.9
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@expo/devcert@1.2.1':
+ dependencies:
+ '@expo/sudo-prompt': 9.3.2
+ debug: 3.2.7
+ transitivePeerDependencies:
+ - supports-color
+
+ '@expo/devtools@55.0.3(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ chalk: 4.1.2
+ optionalDependencies:
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+
+ '@expo/dom-webview@55.0.6(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+
+ '@expo/env@2.1.2':
+ dependencies:
+ chalk: 4.1.2
+ debug: 4.4.3(supports-color@8.1.1)
+ getenv: 2.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@expo/fingerprint@0.16.7':
+ dependencies:
+ '@expo/env': 2.1.2
+ '@expo/spawn-async': 1.7.2
+ arg: 5.0.2
+ chalk: 4.1.2
+ debug: 4.4.3(supports-color@8.1.1)
+ getenv: 2.0.0
+ glob: 13.0.6
+ ignore: 5.3.2
+ minimatch: 10.2.4
+ resolve-from: 5.0.0
+ semver: 7.7.4
+ transitivePeerDependencies:
+ - supports-color
+
+ '@expo/image-utils@0.8.14(typescript@5.9.3)':
+ dependencies:
+ '@expo/require-utils': 55.0.5(typescript@5.9.3)
+ '@expo/spawn-async': 1.7.2
+ chalk: 4.1.2
+ getenv: 2.0.0
+ jimp-compact: 0.16.1
+ parse-png: 2.1.0
+ semver: 7.7.4
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@expo/json-file@10.0.14':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ json5: 2.2.3
+
+ '@expo/local-build-cache-provider@55.0.13(typescript@5.9.3)':
+ dependencies:
+ '@expo/config': 55.0.17(typescript@5.9.3)
+ chalk: 4.1.2
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@expo/log-box@55.0.12(@expo/dom-webview@55.0.6)(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@expo/dom-webview': 55.0.6(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ anser: 1.4.10
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ stacktrace-parser: 0.1.11
+
+ '@expo/metro-config@55.0.21(expo@55.0.24)(typescript@5.9.3)':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/core': 7.27.1
+ '@babel/generator': 7.28.3
+ '@expo/config': 55.0.17(typescript@5.9.3)
+ '@expo/env': 2.1.2
+ '@expo/json-file': 10.0.14
+ '@expo/metro': 55.1.1
+ '@expo/spawn-async': 1.7.2
+ browserslist: 4.26.3
+ chalk: 4.1.2
+ debug: 4.4.3(supports-color@8.1.1)
+ getenv: 2.0.0
+ glob: 13.0.6
+ hermes-parser: 0.32.1
+ jsc-safe-url: 0.2.4
+ lightningcss: 1.32.0
+ picomatch: 4.0.3
+ postcss: 8.4.49
+ resolve-from: 5.0.0
+ optionalDependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - typescript
+ - utf-8-validate
+
+ '@expo/metro-runtime@55.0.11(@expo/dom-webview@55.0.6)(expo@55.0.24)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@expo/log-box': 55.0.12(@expo/dom-webview@55.0.6)(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ anser: 1.4.10
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ pretty-format: 29.7.0
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ stacktrace-parser: 0.1.11
+ whatwg-fetch: 3.6.20
+ optionalDependencies:
+ react-dom: 19.2.0(react@19.2.0)
+ transitivePeerDependencies:
+ - '@expo/dom-webview'
+
+ '@expo/metro@55.1.1':
+ dependencies:
+ metro: 0.83.7
+ metro-babel-transformer: 0.83.7
+ metro-cache: 0.83.7
+ metro-cache-key: 0.83.7
+ metro-config: 0.83.7
+ metro-core: 0.83.7
+ metro-file-map: 0.83.7
+ metro-minify-terser: 0.83.7
+ metro-resolver: 0.83.7
+ metro-runtime: 0.83.7
+ metro-source-map: 0.83.7
+ metro-symbolicate: 0.83.7
+ metro-transform-plugins: 0.83.7
+ metro-transform-worker: 0.83.7
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ '@expo/osascript@2.4.3':
+ dependencies:
+ '@expo/spawn-async': 1.7.2
+
+ '@expo/package-manager@1.10.5':
+ dependencies:
+ '@expo/json-file': 10.0.14
+ '@expo/spawn-async': 1.7.2
+ chalk: 4.1.2
+ npm-package-arg: 11.0.3
+ ora: 3.4.0
+ resolve-workspace-root: 2.0.1
+
+ '@expo/plist@0.5.3':
+ dependencies:
+ '@xmldom/xmldom': 0.8.13
+ base64-js: 1.5.1
+ xmlbuilder: 15.1.1
+
+ '@expo/prebuild-config@55.0.18(expo@55.0.24)(typescript@5.9.3)':
+ dependencies:
+ '@expo/config': 55.0.17(typescript@5.9.3)
+ '@expo/config-plugins': 55.0.9
+ '@expo/config-types': 55.0.5
+ '@expo/image-utils': 0.8.14(typescript@5.9.3)
+ '@expo/json-file': 10.0.14
+ '@react-native/normalize-colors': 0.83.6
+ debug: 4.4.3(supports-color@8.1.1)
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ resolve-from: 5.0.0
+ semver: 7.7.4
+ xml2js: 0.6.0
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@expo/require-utils@55.0.5(typescript@5.9.3)':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/core': 7.27.1
+ '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.27.1)
+ optionalDependencies:
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@expo/router-server@55.0.16(@expo/metro-runtime@55.0.11)(expo-constants@55.0.16)(expo-font@55.0.7)(expo-router@55.0.14)(expo-server@55.0.9)(expo@55.0.24)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ debug: 4.4.3(supports-color@8.1.1)
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-constants: 55.0.16(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))
+ expo-font: 55.0.7(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-server: 55.0.9
+ react: 19.2.0
+ optionalDependencies:
+ '@expo/metro-runtime': 55.0.11(@expo/dom-webview@55.0.6)(expo@55.0.24)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-router: 55.0.14(eed5efedde241c317111b390e3f7dd2b)
+ react-dom: 19.2.0(react@19.2.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@expo/schema-utils@55.0.4': {}
+
+ '@expo/sdk-runtime-versions@1.0.0': {}
+
+ '@expo/spawn-async@1.7.2':
+ dependencies:
+ cross-spawn: 7.0.6
+
+ '@expo/sudo-prompt@9.3.2': {}
+
+ '@expo/vector-icons@15.1.1(expo-font@55.0.7)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ expo-font: 55.0.7(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+
+ '@expo/ws-tunnel@1.0.6': {}
+
+ '@expo/xcpretty@4.4.4':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ chalk: 4.1.2
+ js-yaml: 4.1.0
+
'@fastify/busboy@2.1.1': {}
'@fastify/busboy@3.1.1': {}
@@ -19220,8 +22386,79 @@ snapshots:
'@isaacs/string-locale-compare@1.1.0': {}
+ '@isaacs/ttlcache@1.4.1': {}
+
+ '@istanbuljs/load-nyc-config@1.1.0':
+ dependencies:
+ camelcase: 5.3.1
+ find-up: 4.1.0
+ get-package-type: 0.1.0
+ js-yaml: 3.14.1
+ resolve-from: 5.0.0
+
'@istanbuljs/schema@0.1.3': {}
+ '@jest/create-cache-key-function@29.7.0':
+ dependencies:
+ '@jest/types': 29.6.3
+
+ '@jest/diff-sequences@30.4.0': {}
+
+ '@jest/environment@29.7.0':
+ dependencies:
+ '@jest/fake-timers': 29.7.0
+ '@jest/types': 29.6.3
+ '@types/node': 20.19.21
+ jest-mock: 29.7.0
+
+ '@jest/fake-timers@29.7.0':
+ dependencies:
+ '@jest/types': 29.6.3
+ '@sinonjs/fake-timers': 10.3.0
+ '@types/node': 20.19.21
+ jest-message-util: 29.7.0
+ jest-mock: 29.7.0
+ jest-util: 29.7.0
+
+ '@jest/get-type@30.1.0': {}
+
+ '@jest/schemas@29.6.3':
+ dependencies:
+ '@sinclair/typebox': 0.27.10
+
+ '@jest/schemas@30.4.1':
+ dependencies:
+ '@sinclair/typebox': 0.34.49
+
+ '@jest/transform@29.7.0':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@jest/types': 29.6.3
+ '@jridgewell/trace-mapping': 0.3.31
+ babel-plugin-istanbul: 6.1.1
+ chalk: 4.1.2
+ convert-source-map: 2.0.0
+ fast-json-stable-stringify: 2.1.0
+ graceful-fs: 4.2.11
+ jest-haste-map: 29.7.0
+ jest-regex-util: 29.6.3
+ jest-util: 29.7.0
+ micromatch: 4.0.8
+ pirates: 4.0.7
+ slash: 3.0.0
+ write-file-atomic: 4.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@jest/types@29.6.3':
+ dependencies:
+ '@jest/schemas': 29.6.3
+ '@types/istanbul-lib-coverage': 2.0.6
+ '@types/istanbul-reports': 3.0.4
+ '@types/node': 20.19.21
+ '@types/yargs': 17.0.35
+ chalk: 4.1.2
+
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -19238,7 +22475,6 @@ snapshots:
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
- optional: true
'@jridgewell/source-map@0.3.6':
dependencies:
@@ -19279,9 +22515,9 @@ snapshots:
dependencies:
tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.8.3))
- '@kobalte/tailwindcss@0.9.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3)))':
+ '@kobalte/tailwindcss@0.9.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3)))':
dependencies:
- tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))
+ tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))
'@kobalte/utils@0.9.1(solid-js@1.9.6)':
dependencies:
@@ -19537,8 +22773,8 @@ snapshots:
'@napi-rs/wasm-runtime@0.2.9':
dependencies:
- '@emnapi/core': 1.9.1
- '@emnapi/runtime': 1.9.1
+ '@emnapi/core': 1.10.0
+ '@emnapi/runtime': 1.10.0
'@tybys/wasm-util': 0.9.0
optional: true
@@ -19630,7 +22866,7 @@ snapshots:
'@netlify/zip-it-and-ship-it@10.1.0(encoding@0.1.13)(rollup@4.40.2)':
dependencies:
- '@babel/parser': 7.28.4
+ '@babel/parser': 7.29.3
'@babel/types': 7.27.1
'@netlify/binary-info': 1.0.0
'@netlify/serverless-functions-api': 1.41.1
@@ -19792,7 +23028,7 @@ snapshots:
promise-all-reject-late: 1.0.1
promise-call-limit: 3.0.2
read-package-json-fast: 3.0.2
- semver: 7.7.2
+ semver: 7.7.4
ssri: 10.0.6
treeverse: 3.0.0
walk-up-path: 3.0.1
@@ -19802,7 +23038,7 @@ snapshots:
'@npmcli/fs@3.1.1':
dependencies:
- semver: 7.7.3
+ semver: 7.7.4
'@npmcli/git@5.0.8':
dependencies:
@@ -19836,7 +23072,7 @@ snapshots:
json-parse-even-better-errors: 3.0.2
pacote: 18.0.6
proc-log: 4.2.0
- semver: 7.7.3
+ semver: 7.7.4
transitivePeerDependencies:
- bluebird
- supports-color
@@ -19853,7 +23089,7 @@ snapshots:
json-parse-even-better-errors: 3.0.2
normalize-package-data: 6.0.2
proc-log: 4.2.0
- semver: 7.7.3
+ semver: 7.7.4
transitivePeerDependencies:
- bluebird
@@ -20221,7 +23457,7 @@ snapshots:
'@types/shimmer': 1.2.0
import-in-the-middle: 1.13.1
require-in-the-middle: 7.5.2
- semver: 7.7.3
+ semver: 7.7.4
shimmer: 1.2.1
transitivePeerDependencies:
- supports-color
@@ -20315,7 +23551,7 @@ snapshots:
'@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.0)
'@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0)
- semver: 7.7.2
+ semver: 7.7.4
'@opentelemetry/sdk-trace-node@2.1.0(@opentelemetry/api@1.9.0)':
dependencies:
@@ -20570,16 +23806,16 @@ snapshots:
'@protobufjs/utf8@1.1.0': {}
- '@pulumi/github@6.7.2(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))(typescript@5.8.3)':
+ '@pulumi/github@6.7.2(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))(typescript@5.9.3)':
dependencies:
- '@pulumi/pulumi': 3.201.0(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))(typescript@5.8.3)
+ '@pulumi/pulumi': 3.201.0(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))(typescript@5.9.3)
transitivePeerDependencies:
- bluebird
- supports-color
- ts-node
- typescript
- '@pulumi/pulumi@3.201.0(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))(typescript@5.8.3)':
+ '@pulumi/pulumi@3.201.0(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))(typescript@5.9.3)':
dependencies:
'@grpc/grpc-js': 1.13.3
'@logdna/tail-file': 2.2.0
@@ -20610,15 +23846,15 @@ snapshots:
tmp: 0.2.5
upath: 1.2.0
optionalDependencies:
- ts-node: 10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3)
- typescript: 5.8.3
+ ts-node: 10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3)
+ typescript: 5.9.3
transitivePeerDependencies:
- bluebird
- supports-color
- '@pulumiverse/vercel@1.14.3(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))(typescript@5.8.3)':
+ '@pulumiverse/vercel@1.14.3(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))(typescript@5.9.3)':
dependencies:
- '@pulumi/pulumi': 3.201.0(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))(typescript@5.8.3)
+ '@pulumi/pulumi': 3.201.0(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))(typescript@5.9.3)
transitivePeerDependencies:
- bluebird
- supports-color
@@ -20639,6 +23875,8 @@ snapshots:
'@radix-ui/primitive@1.1.2': {}
+ '@radix-ui/primitive@1.1.3': {}
+
'@radix-ui/react-arrow@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -20669,6 +23907,18 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
@@ -20686,6 +23936,12 @@ snapshots:
'@babel/runtime': 7.27.1
react: 19.2.4
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
react: 19.2.4
@@ -20697,6 +23953,12 @@ snapshots:
'@babel/runtime': 7.27.1
react: 19.2.4
+ '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
react: 19.2.4
@@ -20725,6 +23987,28 @@ snapshots:
transitivePeerDependencies:
- '@types/react'
+ '@radix-ui/react-dialog@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-dismissable-layer': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-focus-scope': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-portal': 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-slot': 1.2.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.0)
+ aria-hidden: 1.2.4
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ react-remove-scroll: 2.6.3(@types/react@19.2.14)(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-dialog@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.2
@@ -20747,6 +24031,12 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
react: 19.2.4
@@ -20777,6 +24067,19 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@radix-ui/react-dismissable-layer@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-dismissable-layer@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.2
@@ -20810,6 +24113,12 @@ snapshots:
'@babel/runtime': 7.27.1
react: 19.2.4
+ '@radix-ui/react-focus-guards@1.1.2(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-focus-guards@1.1.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
react: 19.2.4
@@ -20825,6 +24134,17 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-focus-scope@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-focus-scope@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
@@ -20853,6 +24173,13 @@ snapshots:
'@radix-ui/react-use-layout-effect': 1.0.0(react@19.2.4)
react: 19.2.4
+ '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
@@ -20983,6 +24310,16 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-portal@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-portal@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -21011,6 +24348,16 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-presence@1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-presence@1.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
@@ -21021,6 +24368,16 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-primitive@1.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.27.1
@@ -21028,6 +24385,15 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
+ '@radix-ui/react-primitive@2.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-slot': 1.2.2(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-primitive@2.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-slot': 1.2.2(@types/react@19.2.14)(react@19.2.4)
@@ -21037,6 +24403,15 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
@@ -21046,6 +24421,23 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-roving-focus@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.2
@@ -21117,6 +24509,13 @@ snapshots:
'@radix-ui/react-compose-refs': 1.0.0(react@19.2.4)
react: 19.2.4
+ '@radix-ui/react-slot@1.2.2(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-slot@1.2.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
@@ -21124,6 +24523,13 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
+ '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
@@ -21146,6 +24552,22 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
+ '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
'@radix-ui/react-tooltip@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.2
@@ -21171,6 +24593,12 @@ snapshots:
'@babel/runtime': 7.27.1
react: 19.2.4
+ '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
react: 19.2.4
@@ -21183,6 +24611,14 @@ snapshots:
'@radix-ui/react-use-callback-ref': 1.0.0(react@19.2.4)
react: 19.2.4
+ '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4)
@@ -21191,6 +24627,13 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
+ '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
@@ -21204,6 +24647,13 @@ snapshots:
'@radix-ui/react-use-callback-ref': 1.0.0(react@19.2.4)
react: 19.2.4
+ '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.0)
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
@@ -21216,6 +24666,12 @@ snapshots:
'@babel/runtime': 7.27.1
react: 19.2.4
+ '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.0)':
+ dependencies:
+ react: 19.2.0
+ optionalDependencies:
+ '@types/react': 19.2.14
+
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
react: 19.2.4
@@ -21486,6 +24942,283 @@ snapshots:
dependencies:
react: 19.2.4
+ '@react-native/assets-registry@0.83.6': {}
+
+ '@react-native/babel-plugin-codegen@0.83.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@react-native/codegen': 0.83.6(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - '@babel/core'
+ - supports-color
+
+ '@react-native/babel-plugin-codegen@0.85.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@react-native/codegen': 0.85.3(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - '@babel/core'
+ - supports-color
+ optional: true
+
+ '@react-native/babel-preset@0.83.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.27.1)
+ '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.27.1)
+ '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.27.1)
+ '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.27.1)
+ '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.27.1)
+ '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.27.1)
+ '@babel/template': 7.28.6
+ '@react-native/babel-plugin-codegen': 0.83.6(@babel/core@7.27.1)
+ babel-plugin-syntax-hermes-parser: 0.32.0
+ babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.27.1)
+ react-refresh: 0.14.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@react-native/babel-preset@0.85.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.27.1)
+ '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.27.1)
+ '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.27.1)
+ '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.27.1)
+ '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.27.1)
+ '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.27.1)
+ '@react-native/babel-plugin-codegen': 0.85.3(@babel/core@7.27.1)
+ babel-plugin-syntax-hermes-parser: 0.33.3
+ babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.27.1)
+ react-refresh: 0.14.2
+ transitivePeerDependencies:
+ - supports-color
+ optional: true
+
+ '@react-native/codegen@0.83.6(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/parser': 7.29.3
+ glob: 7.2.3
+ hermes-parser: 0.32.0
+ invariant: 2.2.4
+ nullthrows: 1.1.1
+ yargs: 17.7.2
+
+ '@react-native/codegen@0.85.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/parser': 7.29.3
+ hermes-parser: 0.33.3
+ invariant: 2.2.4
+ nullthrows: 1.1.1
+ tinyglobby: 0.2.15
+ yargs: 17.7.2
+ optional: true
+
+ '@react-native/community-cli-plugin@0.83.6(@react-native/metro-config@0.85.3(@babel/core@7.27.1))':
+ dependencies:
+ '@react-native/dev-middleware': 0.83.6
+ debug: 4.4.3(supports-color@8.1.1)
+ invariant: 2.2.4
+ metro: 0.83.7
+ metro-config: 0.83.7
+ metro-core: 0.83.7
+ semver: 7.7.4
+ optionalDependencies:
+ '@react-native/metro-config': 0.85.3(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ '@react-native/debugger-frontend@0.83.6': {}
+
+ '@react-native/debugger-shell@0.83.6':
+ dependencies:
+ cross-spawn: 7.0.6
+ fb-dotslash: 0.5.8
+
+ '@react-native/dev-middleware@0.83.6':
+ dependencies:
+ '@isaacs/ttlcache': 1.4.1
+ '@react-native/debugger-frontend': 0.83.6
+ '@react-native/debugger-shell': 0.83.6
+ chrome-launcher: 0.15.2
+ chromium-edge-launcher: 0.2.0
+ connect: 3.7.0
+ debug: 4.4.3(supports-color@8.1.1)
+ invariant: 2.2.4
+ nullthrows: 1.1.1
+ open: 7.4.2
+ serve-static: 1.16.2
+ ws: 7.5.10
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ '@react-native/gradle-plugin@0.83.6': {}
+
+ '@react-native/js-polyfills@0.83.6': {}
+
+ '@react-native/js-polyfills@0.85.3':
+ optional: true
+
+ '@react-native/metro-babel-transformer@0.85.3(@babel/core@7.27.1)':
+ dependencies:
+ '@babel/core': 7.27.1
+ '@react-native/babel-preset': 0.85.3(@babel/core@7.27.1)
+ hermes-parser: 0.33.3
+ nullthrows: 1.1.1
+ transitivePeerDependencies:
+ - supports-color
+ optional: true
+
+ '@react-native/metro-config@0.85.3(@babel/core@7.27.1)':
+ dependencies:
+ '@react-native/js-polyfills': 0.85.3
+ '@react-native/metro-babel-transformer': 0.85.3(@babel/core@7.27.1)
+ metro-config: 0.84.4
+ metro-runtime: 0.84.4
+ transitivePeerDependencies:
+ - '@babel/core'
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ optional: true
+
+ '@react-native/normalize-colors@0.74.89': {}
+
+ '@react-native/normalize-colors@0.83.6': {}
+
+ '@react-native/virtualized-lists@0.83.6(@types/react@19.2.14)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ invariant: 2.2.4
+ nullthrows: 1.1.1
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@react-navigation/bottom-tabs@7.16.1(@react-navigation/native@7.2.4(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-screens@4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@react-navigation/elements': 2.9.18(@react-navigation/native@7.2.4(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@react-navigation/native': 7.2.4(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ color: 4.2.3
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ react-native-safe-area-context: 5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-screens: 4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ sf-symbols-typescript: 2.2.0
+ transitivePeerDependencies:
+ - '@react-native-masked-view/masked-view'
+
+ '@react-navigation/core@7.17.4(react@19.2.0)':
+ dependencies:
+ '@react-navigation/routers': 7.5.5
+ escape-string-regexp: 4.0.0
+ fast-deep-equal: 3.1.3
+ nanoid: 3.3.11
+ query-string: 7.1.3
+ react: 19.2.0
+ react-is: 19.2.6
+ use-latest-callback: 0.2.6(react@19.2.0)
+ use-sync-external-store: 1.5.0(react@19.2.0)
+
+ '@react-navigation/elements@2.9.18(@react-navigation/native@7.2.4(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@react-navigation/native': 7.2.4(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ color: 4.2.3
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ react-native-safe-area-context: 5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ use-latest-callback: 0.2.6(react@19.2.0)
+ use-sync-external-store: 1.5.0(react@19.2.0)
+
+ '@react-navigation/native-stack@7.15.1(@react-navigation/native@7.2.4(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-screens@4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@react-navigation/elements': 2.9.18(@react-navigation/native@7.2.4(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@react-navigation/native': 7.2.4(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ color: 4.2.3
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ react-native-safe-area-context: 5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-screens: 4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ sf-symbols-typescript: 2.2.0
+ warn-once: 0.1.1
+ transitivePeerDependencies:
+ - '@react-native-masked-view/masked-view'
+
+ '@react-navigation/native@7.2.4(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@react-navigation/core': 7.17.4(react@19.2.0)
+ escape-string-regexp: 4.0.0
+ fast-deep-equal: 3.1.3
+ nanoid: 3.3.11
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ use-latest-callback: 0.2.6(react@19.2.0)
+
+ '@react-navigation/routers@7.5.5':
+ dependencies:
+ nanoid: 3.3.11
+
'@reduxjs/toolkit@2.10.1(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)':
dependencies:
'@standard-schema/spec': 1.0.0
@@ -21626,7 +25359,7 @@ snapshots:
estree-walker: 2.0.2
fdir: 6.5.0(picomatch@4.0.3)
is-reference: 1.2.1
- magic-string: 0.30.19
+ magic-string: 0.30.21
picomatch: 4.0.3
optionalDependencies:
rollup: 4.40.2
@@ -21635,7 +25368,7 @@ snapshots:
dependencies:
'@rollup/pluginutils': 5.1.4(rollup@4.40.2)
estree-walker: 2.0.2
- magic-string: 0.30.19
+ magic-string: 0.30.21
optionalDependencies:
rollup: 4.40.2
@@ -21658,7 +25391,7 @@ snapshots:
'@rollup/plugin-replace@6.0.2(rollup@4.40.2)':
dependencies:
'@rollup/pluginutils': 5.1.4(rollup@4.40.2)
- magic-string: 0.30.19
+ magic-string: 0.30.21
optionalDependencies:
rollup: 4.40.2
@@ -21678,7 +25411,7 @@ snapshots:
dependencies:
'@types/estree': 1.0.7
estree-walker: 2.0.2
- picomatch: 4.0.2
+ picomatch: 4.0.3
optionalDependencies:
rollup: 4.40.2
@@ -21848,6 +25581,13 @@ snapshots:
'@shinyoshiaki/jspack@0.0.6': {}
+ '@shopify/flash-list@2.0.2(@babel/runtime@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@babel/runtime': 7.27.1
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ tslib: 2.8.1
+
'@sigstore/bundle@2.3.2':
dependencies:
'@sigstore/protobuf-specs': 0.3.3
@@ -21880,6 +25620,10 @@ snapshots:
'@sigstore/core': 1.1.0
'@sigstore/protobuf-specs': 0.3.3
+ '@sinclair/typebox@0.27.10': {}
+
+ '@sinclair/typebox@0.34.49': {}
+
'@sindresorhus/is@4.6.0': {}
'@sindresorhus/is@5.6.0': {}
@@ -21888,6 +25632,14 @@ snapshots:
'@sindresorhus/merge-streams@2.3.0': {}
+ '@sinonjs/commons@3.0.1':
+ dependencies:
+ type-detect: 4.0.8
+
+ '@sinonjs/fake-timers@10.3.0':
+ dependencies:
+ '@sinonjs/commons': 3.0.1
+
'@smithy/abort-controller@4.0.2':
dependencies:
'@smithy/types': 4.3.1
@@ -22921,11 +26673,11 @@ snapshots:
dependencies:
solid-js: 1.9.6
- '@solidjs/start@1.1.3(@testing-library/jest-dom@6.5.0)(@types/node@22.15.17)(jiti@2.6.1)(solid-js@1.9.6)(terser@5.44.0)(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)':
+ '@solidjs/start@1.1.3(@testing-library/jest-dom@6.5.0)(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(solid-js@1.9.6)(terser@5.44.0)(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)':
dependencies:
- '@tanstack/server-functions-plugin': 1.119.2(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
- '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))
- '@vinxi/server-components': 0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))
+ '@tanstack/server-functions-plugin': 1.119.2(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
+ '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))
+ '@vinxi/server-components': 0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))
defu: 6.1.4
error-stack-parser: 2.1.4
html-to-image: 1.11.13
@@ -22936,8 +26688,8 @@ snapshots:
source-map-js: 1.2.1
terracotta: 1.0.6(solid-js@1.9.6)
tinyglobby: 0.2.13
- vinxi: 0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1)
- vite-plugin-solid: 2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))
+ vinxi: 0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1)
+ vite-plugin-solid: 2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
transitivePeerDependencies:
- '@testing-library/jest-dom'
- '@types/node'
@@ -23066,12 +26818,12 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
- '@storybook/builder-vite@10.5.0-alpha.0(esbuild@0.25.5)(rollup@4.40.2)(storybook@8.6.12(prettier@3.7.4))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))':
+ '@storybook/builder-vite@10.5.0-alpha.0(esbuild@0.25.5)(rollup@4.40.2)(storybook@8.6.12(prettier@3.7.4))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))':
dependencies:
- '@storybook/csf-plugin': 10.5.0-alpha.0(esbuild@0.25.5)(rollup@4.40.2)(storybook@8.6.12(prettier@3.7.4))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))
+ '@storybook/csf-plugin': 10.5.0-alpha.0(esbuild@0.25.5)(rollup@4.40.2)(storybook@8.6.12(prettier@3.7.4))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))
storybook: 8.6.12(prettier@3.7.4)
ts-dedent: 2.2.0
- vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- esbuild
- rollup
@@ -23087,7 +26839,7 @@ snapshots:
jsdoc-type-pratt-parser: 4.1.0
process: 0.11.10
recast: 0.23.11
- semver: 7.7.2
+ semver: 7.7.4
util: 0.12.5
ws: 8.18.3
optionalDependencies:
@@ -23098,14 +26850,14 @@ snapshots:
- supports-color
- utf-8-validate
- '@storybook/csf-plugin@10.5.0-alpha.0(esbuild@0.25.5)(rollup@4.40.2)(storybook@8.6.12(prettier@3.7.4))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))':
+ '@storybook/csf-plugin@10.5.0-alpha.0(esbuild@0.25.5)(rollup@4.40.2)(storybook@8.6.12(prettier@3.7.4))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))':
dependencies:
storybook: 8.6.12(prettier@3.7.4)
unplugin: 2.3.11
optionalDependencies:
esbuild: 0.25.5
rollup: 4.40.2
- vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
webpack: 5.101.3(esbuild@0.25.5)
'@storybook/csf-plugin@8.6.12(storybook@8.6.12(prettier@3.7.4))':
@@ -23312,6 +27064,12 @@ snapshots:
valibot: 1.0.0-rc.1(typescript@5.8.3)
zod: 3.25.76
+ '@t3-oss/env-core@0.12.0(typescript@5.9.3)(valibot@1.0.0-rc.1(typescript@5.9.3))(zod@3.25.76)':
+ optionalDependencies:
+ typescript: 5.9.3
+ valibot: 1.0.0-rc.1(typescript@5.9.3)
+ zod: 3.25.76
+
'@t3-oss/env-nextjs@0.12.0(typescript@5.8.3)(valibot@1.0.0-rc.1(typescript@5.8.3))(zod@3.25.76)':
dependencies:
'@t3-oss/env-core': 0.12.0(typescript@5.8.3)(valibot@1.0.0-rc.1(typescript@5.8.3))(zod@3.25.76)
@@ -23320,6 +27078,14 @@ snapshots:
valibot: 1.0.0-rc.1(typescript@5.8.3)
zod: 3.25.76
+ '@t3-oss/env-nextjs@0.12.0(typescript@5.9.3)(valibot@1.0.0-rc.1(typescript@5.9.3))(zod@3.25.76)':
+ dependencies:
+ '@t3-oss/env-core': 0.12.0(typescript@5.9.3)(valibot@1.0.0-rc.1(typescript@5.9.3))(zod@3.25.76)
+ optionalDependencies:
+ typescript: 5.9.3
+ valibot: 1.0.0-rc.1(typescript@5.9.3)
+ zod: 3.25.76
+
'@tailwindcss/typography@0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.8.3)))':
dependencies:
lodash.castarray: 4.4.0
@@ -23328,13 +27094,13 @@ snapshots:
postcss-selector-parser: 6.0.10
tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.8.3))
- '@tailwindcss/typography@0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3)))':
+ '@tailwindcss/typography@0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3)))':
dependencies:
lodash.castarray: 4.4.0
lodash.isplainobject: 4.0.6
lodash.merge: 4.6.2
postcss-selector-parser: 6.0.10
- tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))
+ tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))
'@tanstack/devtools-event-bus@0.3.2':
dependencies:
@@ -23364,20 +27130,20 @@ snapshots:
- csstype
- utf-8-validate
- '@tanstack/directive-functions-plugin@1.119.2(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)':
+ '@tanstack/directive-functions-plugin@1.119.2(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)':
dependencies:
'@babel/code-frame': 7.26.2
'@babel/core': 7.27.1
- '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.27.1)
'@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.27.1)
- '@babel/template': 7.27.2
- '@babel/traverse': 7.27.4
- '@babel/types': 7.27.1
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
'@tanstack/router-utils': 1.115.0
babel-dead-code-elimination: 1.0.10
dedent: 1.6.0
tiny-invariant: 1.3.3
- vite: 6.1.4(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ vite: 6.1.4(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@@ -23444,12 +27210,12 @@ snapshots:
'@tanstack/router-utils@1.115.0':
dependencies:
- '@babel/generator': 7.28.3
- '@babel/parser': 7.28.4
+ '@babel/generator': 7.29.1
+ '@babel/parser': 7.29.3
ansis: 3.17.0
diff: 7.0.0
- '@tanstack/server-functions-plugin@1.119.2(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)':
+ '@tanstack/server-functions-plugin@1.119.2(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)':
dependencies:
'@babel/code-frame': 7.26.2
'@babel/core': 7.27.1
@@ -23458,7 +27224,7 @@ snapshots:
'@babel/template': 7.27.2
'@babel/traverse': 7.27.1
'@babel/types': 7.27.1
- '@tanstack/directive-functions-plugin': 1.119.2(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ '@tanstack/directive-functions-plugin': 1.119.2(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
babel-dead-code-elimination: 1.0.10
dedent: 1.6.0
tiny-invariant: 1.3.3
@@ -23664,6 +27430,16 @@ snapshots:
lodash: 4.17.21
redent: 3.0.0
+ '@testing-library/react-native@13.3.3(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react-test-renderer@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ jest-matcher-utils: 30.4.1
+ picocolors: 1.1.1
+ pretty-format: 30.4.1
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ react-test-renderer: 19.2.0(react@19.2.0)
+ redent: 3.0.0
+
'@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)':
dependencies:
'@testing-library/dom': 10.4.0
@@ -23750,16 +27526,16 @@ snapshots:
'@types/babel__generator@7.27.0':
dependencies:
- '@babel/types': 7.27.1
+ '@babel/types': 7.28.4
'@types/babel__template@7.4.4':
dependencies:
- '@babel/parser': 7.27.5
- '@babel/types': 7.27.1
+ '@babel/parser': 7.28.4
+ '@babel/types': 7.28.4
'@types/babel__traverse@7.20.7':
dependencies:
- '@babel/types': 7.27.1
+ '@babel/types': 7.28.4
'@types/body-parser@1.19.5':
dependencies:
@@ -23883,6 +27659,12 @@ snapshots:
'@types/google-protobuf@3.15.12': {}
+ '@types/graceful-fs@4.1.9':
+ dependencies:
+ '@types/node': 20.19.21
+
+ '@types/hammerjs@2.0.46': {}
+
'@types/hast@3.0.4':
dependencies:
'@types/unist': 3.0.3
@@ -23893,6 +27675,16 @@ snapshots:
'@types/http-errors@2.0.4': {}
+ '@types/istanbul-lib-coverage@2.0.6': {}
+
+ '@types/istanbul-lib-report@3.0.3':
+ dependencies:
+ '@types/istanbul-lib-coverage': 2.0.6
+
+ '@types/istanbul-reports@3.0.4':
+ dependencies:
+ '@types/istanbul-lib-report': 3.0.3
+
'@types/js-cookie@3.0.6': {}
'@types/jsdom@21.1.7':
@@ -23990,6 +27782,10 @@ snapshots:
dependencies:
'@types/react': 19.2.14
+ '@types/react-test-renderer@19.1.0':
+ dependencies:
+ '@types/react': 19.2.14
+
'@types/react-tooltip@4.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
react-tooltip: 5.28.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -24026,6 +27822,8 @@ snapshots:
'@types/shimmer@1.2.0': {}
+ '@types/stack-utils@2.0.3': {}
+
'@types/tmp@0.2.6': {}
'@types/tough-cookie@4.0.5': {}
@@ -24045,27 +27843,33 @@ snapshots:
'@types/uuid@9.0.8': {}
+ '@types/yargs-parser@21.0.3': {}
+
+ '@types/yargs@17.0.35':
+ dependencies:
+ '@types/yargs-parser': 21.0.3
+
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 20.19.21
optional: true
- '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)':
+ '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
- '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3)
+ '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/scope-manager': 5.62.0
- '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.1)(typescript@5.8.3)
- '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.8.3)
+ '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.1)(typescript@5.9.3)
+ '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.9.3)
debug: 4.4.0
eslint: 8.57.1
graphemer: 1.4.0
ignore: 5.3.2
natural-compare-lite: 1.4.0
semver: 7.7.1
- tsutils: 3.21.0(typescript@5.8.3)
+ tsutils: 3.21.0(typescript@5.9.3)
optionalDependencies:
- typescript: 5.8.3
+ typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -24085,15 +27889,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3)':
+ '@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 5.62.0
'@typescript-eslint/types': 5.62.0
- '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.3)
+ '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.9.3)
debug: 4.4.0
eslint: 8.57.1
optionalDependencies:
- typescript: 5.8.3
+ typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -24132,15 +27936,15 @@ snapshots:
dependencies:
typescript: 5.8.3
- '@typescript-eslint/type-utils@5.62.0(eslint@8.57.1)(typescript@5.8.3)':
+ '@typescript-eslint/type-utils@5.62.0(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
- '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.3)
- '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.8.3)
+ '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.9.3)
+ '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.9.3)
debug: 4.4.3(supports-color@8.1.1)
eslint: 8.57.1
- tsutils: 3.21.0(typescript@5.8.3)
+ tsutils: 3.21.0(typescript@5.9.3)
optionalDependencies:
- typescript: 5.8.3
+ typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -24160,17 +27964,17 @@ snapshots:
'@typescript-eslint/types@8.57.2': {}
- '@typescript-eslint/typescript-estree@5.62.0(typescript@5.8.3)':
+ '@typescript-eslint/typescript-estree@5.62.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 5.62.0
'@typescript-eslint/visitor-keys': 5.62.0
debug: 4.4.3(supports-color@8.1.1)
globby: 11.1.0
is-glob: 4.0.3
- semver: 7.7.1
- tsutils: 3.21.0(typescript@5.8.3)
+ semver: 7.7.4
+ tsutils: 3.21.0(typescript@5.9.3)
optionalDependencies:
- typescript: 5.8.3
+ typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -24189,17 +27993,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.8.3)':
+ '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1)
'@types/json-schema': 7.0.15
'@types/semver': 7.7.0
'@typescript-eslint/scope-manager': 5.62.0
'@typescript-eslint/types': 5.62.0
- '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.3)
+ '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.9.3)
eslint: 8.57.1
eslint-scope: 5.1.1
- semver: 7.7.1
+ semver: 7.7.4
transitivePeerDependencies:
- supports-color
- typescript
@@ -24318,8 +28122,8 @@ snapshots:
dependencies:
'@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13)
'@rollup/pluginutils': 5.1.4(rollup@4.40.2)
- acorn: 8.15.0
- acorn-import-attributes: 1.9.5(acorn@8.15.0)
+ acorn: 8.16.0
+ acorn-import-attributes: 1.9.5(acorn@8.16.0)
async-sema: 3.1.1
bindings: 1.5.0
estree-walker: 2.0.2
@@ -24396,7 +28200,7 @@ snapshots:
untun: 0.1.3
uqr: 0.1.2
- '@vinxi/plugin-directives@0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))':
+ '@vinxi/plugin-directives@0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))':
dependencies:
'@babel/parser': 7.27.2
acorn: 8.14.1
@@ -24407,18 +28211,18 @@ snapshots:
magicast: 0.2.11
recast: 0.23.11
tslib: 2.8.1
- vinxi: 0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1)
+ vinxi: 0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1)
- '@vinxi/server-components@0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))':
+ '@vinxi/server-components@0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))':
dependencies:
- '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))
+ '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1))
acorn: 8.14.1
acorn-loose: 8.5.0
acorn-typescript: 1.4.13(acorn@8.14.1)
astring: 1.9.0
magicast: 0.2.11
recast: 0.23.11
- vinxi: 0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1)
+ vinxi: 0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1)
'@virtual-grid/core@2.0.1': {}
@@ -24436,14 +28240,14 @@ snapshots:
'@virtual-grid/shared@2.0.1': {}
- '@vitejs/plugin-react@4.4.1(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))':
+ '@vitejs/plugin-react@4.4.1(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))':
dependencies:
'@babel/core': 7.27.1
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.1)
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.1)
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
- vite: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ vite: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- supports-color
@@ -24462,7 +28266,7 @@ snapshots:
std-env: 3.9.0
test-exclude: 7.0.1
tinyrainbow: 2.0.0
- vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.43)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.44.0)(yaml@2.8.1)
+ vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.43)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- supports-color
@@ -24488,21 +28292,29 @@ snapshots:
chai: 5.2.0
tinyrainbow: 2.0.0
- '@vitest/mocker@2.1.9(vite@5.4.19(@types/node@22.15.17)(terser@5.44.0))':
+ '@vitest/mocker@2.1.9(vite@5.4.19(@types/node@22.15.17)(lightningcss@1.32.0)(terser@5.44.0))':
dependencies:
'@vitest/spy': 2.1.9
estree-walker: 3.0.3
- magic-string: 0.30.17
+ magic-string: 0.30.21
optionalDependencies:
- vite: 5.4.19(@types/node@22.15.17)(terser@5.44.0)
+ vite: 5.4.19(@types/node@22.15.17)(lightningcss@1.32.0)(terser@5.44.0)
- '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))':
+ '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
- magic-string: 0.30.19
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
+
+ '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))':
+ dependencies:
+ '@vitest/spy': 3.2.4
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
optionalDependencies:
- vite: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
'@vitest/pretty-format@2.0.5':
dependencies:
@@ -24530,13 +28342,13 @@ snapshots:
'@vitest/snapshot@2.1.9':
dependencies:
'@vitest/pretty-format': 2.1.9
- magic-string: 0.30.17
+ magic-string: 0.30.21
pathe: 1.1.2
'@vitest/snapshot@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
- magic-string: 0.30.19
+ magic-string: 0.30.21
pathe: 2.0.3
'@vitest/spy@2.0.5':
@@ -24560,13 +28372,13 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 2.0.0
- vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.43)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.44.0)(yaml@2.8.1)
+ vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.15.17)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
'@vitest/utils@2.0.5':
dependencies:
'@vitest/pretty-format': 2.0.5
estree-walker: 3.0.3
- loupe: 3.1.3
+ loupe: 3.2.1
tinyrainbow: 1.2.0
'@vitest/utils@2.1.9':
@@ -24821,7 +28633,7 @@ snapshots:
- aws-crt
- supports-color
- '@workflow/next@4.0.1-beta.69(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))':
+ '@workflow/next@4.0.1-beta.69(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))':
dependencies:
'@swc/core': 1.15.3(@swc/helpers@0.5.17)
'@workflow/builders': 4.0.1-beta.64(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)
@@ -24830,7 +28642,7 @@ snapshots:
semver: 7.7.4
watchpack: 2.5.1
optionalDependencies:
- next: 16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ next: 16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
transitivePeerDependencies:
- '@opentelemetry/api'
- '@swc/helpers'
@@ -24951,9 +28763,9 @@ snapshots:
ulid: 3.0.1
zod: 4.3.6
- '@workos-inc/node@7.50.0(express@5.1.0)(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))':
+ '@workos-inc/node@7.50.0(express@5.1.0)(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))':
dependencies:
- iron-session: 6.3.1(express@5.1.0)(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
+ iron-session: 6.3.1(express@5.1.0)(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
jose: 5.6.3
leb: 1.0.0
pluralize: 8.0.0
@@ -25045,6 +28857,10 @@ snapshots:
dependencies:
arch: 3.0.0
+ '@xmldom/xmldom@0.8.13': {}
+
+ '@xmldom/xmldom@0.9.10': {}
+
'@xtuc/ieee754@1.2.0':
optional: true
@@ -25075,6 +28891,10 @@ snapshots:
dependencies:
acorn: 8.15.0
+ acorn-import-attributes@1.9.5(acorn@8.16.0):
+ dependencies:
+ acorn: 8.16.0
+
acorn-import-phases@1.0.4(acorn@8.16.0):
dependencies:
acorn: 8.16.0
@@ -25161,6 +28981,8 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
+ anser@1.4.10: {}
+
ansi-align@3.0.1:
dependencies:
string-width: 4.2.3
@@ -25175,6 +28997,8 @@ snapshots:
dependencies:
environment: 1.1.0
+ ansi-regex@4.1.1: {}
+
ansi-regex@5.0.1: {}
ansi-regex@6.1.0: {}
@@ -25376,6 +29200,8 @@ snapshots:
dependencies:
printable-characters: 1.0.42
+ asap@2.0.6: {}
+
asn1js@3.0.6:
dependencies:
pvtsutils: 1.3.6
@@ -25386,7 +29212,7 @@ snapshots:
ast-kit@2.1.3:
dependencies:
- '@babel/parser': 7.28.4
+ '@babel/parser': 7.29.3
pathe: 2.0.3
ast-module-types@5.0.0: {}
@@ -25469,22 +29295,159 @@ snapshots:
babel-dead-code-elimination@1.0.10:
dependencies:
'@babel/core': 7.27.1
- '@babel/parser': 7.27.5
- '@babel/traverse': 7.27.4
- '@babel/types': 7.27.1
+ '@babel/parser': 7.29.3
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-jest@29.7.0(@babel/core@7.27.1):
+ dependencies:
+ '@babel/core': 7.27.1
+ '@jest/transform': 29.7.0
+ '@types/babel__core': 7.20.5
+ babel-plugin-istanbul: 6.1.1
+ babel-preset-jest: 29.6.3(@babel/core@7.27.1)
+ chalk: 4.1.2
+ graceful-fs: 4.2.11
+ slash: 3.0.0
transitivePeerDependencies:
- supports-color
+ babel-plugin-istanbul@6.1.1:
+ dependencies:
+ '@babel/helper-plugin-utils': 7.28.6
+ '@istanbuljs/load-nyc-config': 1.1.0
+ '@istanbuljs/schema': 0.1.3
+ istanbul-lib-instrument: 5.2.1
+ test-exclude: 6.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-jest-hoist@29.6.3:
+ dependencies:
+ '@babel/template': 7.28.6
+ '@babel/types': 7.29.0
+ '@types/babel__core': 7.20.5
+ '@types/babel__traverse': 7.20.7
+
babel-plugin-jsx-dom-expressions@0.39.8(@babel/core@7.27.1):
dependencies:
'@babel/core': 7.27.1
'@babel/helper-module-imports': 7.18.6
- '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1)
- '@babel/types': 7.27.1
+ '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.27.1)
+ '@babel/types': 7.29.0
html-entities: 2.3.3
parse5: 7.3.0
validate-html-nesting: 1.2.2
+ babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.27.1):
+ dependencies:
+ '@babel/compat-data': 7.29.3
+ '@babel/core': 7.27.1
+ '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.27.1)
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.27.1):
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.27.1)
+ core-js-compat: 3.49.0
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.27.1):
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-react-compiler@1.0.0:
+ dependencies:
+ '@babel/types': 7.29.0
+
+ babel-plugin-react-native-web@0.21.2: {}
+
+ babel-plugin-syntax-hermes-parser@0.32.0:
+ dependencies:
+ hermes-parser: 0.32.0
+
+ babel-plugin-syntax-hermes-parser@0.32.1:
+ dependencies:
+ hermes-parser: 0.32.1
+
+ babel-plugin-syntax-hermes-parser@0.33.3:
+ dependencies:
+ hermes-parser: 0.33.3
+ optional: true
+
+ babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.27.1):
+ dependencies:
+ '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.27.1)
+ transitivePeerDependencies:
+ - '@babel/core'
+
+ babel-preset-current-node-syntax@1.2.0(@babel/core@7.27.1):
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.27.1)
+ '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.27.1)
+ '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.27.1)
+ '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.27.1)
+ '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.27.1)
+ '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.27.1)
+ '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.27.1)
+ '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.27.1)
+ '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.27.1)
+
+ babel-preset-expo@55.0.21(@babel/core@7.27.1)(@babel/runtime@7.27.1)(expo@55.0.24)(react-refresh@0.14.2):
+ dependencies:
+ '@babel/generator': 7.29.1
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.27.1)
+ '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.27.1)
+ '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.27.1)
+ '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.27.1)
+ '@babel/preset-react': 7.28.5(@babel/core@7.27.1)
+ '@babel/preset-typescript': 7.28.5(@babel/core@7.27.1)
+ '@react-native/babel-preset': 0.83.6(@babel/core@7.27.1)
+ babel-plugin-react-compiler: 1.0.0
+ babel-plugin-react-native-web: 0.21.2
+ babel-plugin-syntax-hermes-parser: 0.32.1
+ babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.27.1)
+ debug: 4.4.3(supports-color@8.1.1)
+ react-refresh: 0.14.2
+ resolve-from: 5.0.0
+ optionalDependencies:
+ '@babel/runtime': 7.27.1
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ transitivePeerDependencies:
+ - '@babel/core'
+ - supports-color
+
+ babel-preset-jest@29.6.3(@babel/core@7.27.1):
+ dependencies:
+ '@babel/core': 7.27.1
+ babel-plugin-jest-hoist: 29.6.3
+ babel-preset-current-node-syntax: 1.2.0(@babel/core@7.27.1)
+
babel-preset-solid@1.9.6(@babel/core@7.27.1):
dependencies:
'@babel/core': 7.27.1
@@ -25505,7 +29468,7 @@ snapshots:
baseline-browser-mapping@2.10.11: {}
- baseline-browser-mapping@2.8.16: {}
+ baseline-browser-mapping@2.10.30: {}
before-after-hook@2.2.3: {}
@@ -25517,6 +29480,8 @@ snapshots:
bezier-easing@2.1.0: {}
+ big-integer@1.6.52: {}
+
bin-links@4.0.4:
dependencies:
cmd-shim: 6.0.3
@@ -25577,7 +29542,7 @@ snapshots:
bytes: 3.1.2
content-type: 1.0.5
debug: 4.4.3(supports-color@8.1.1)
- http-errors: 2.0.0
+ http-errors: 2.0.1
iconv-lite: 0.6.3
on-finished: 2.4.1
qs: 6.14.0
@@ -25586,6 +29551,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ boolbase@1.0.0: {}
+
bottleneck@2.19.5: {}
bowser@2.11.0: {}
@@ -25594,13 +29561,25 @@ snapshots:
dependencies:
ansi-align: 3.0.1
camelcase: 8.0.0
- chalk: 5.4.1
+ chalk: 5.6.2
cli-boxes: 3.0.0
string-width: 7.2.0
type-fest: 4.41.0
widest-line: 5.0.0
wrap-ansi: 9.0.0
+ bplist-creator@0.1.0:
+ dependencies:
+ stream-buffers: 2.2.0
+
+ bplist-parser@0.3.1:
+ dependencies:
+ big-integer: 1.6.52
+
+ bplist-parser@0.3.2:
+ dependencies:
+ big-integer: 1.6.52
+
brace-expansion@1.1.11:
dependencies:
balanced-match: 1.0.2
@@ -25629,12 +29608,24 @@ snapshots:
browserslist@4.26.3:
dependencies:
- baseline-browser-mapping: 2.8.16
+ baseline-browser-mapping: 2.10.11
caniuse-lite: 1.0.30001750
electron-to-chromium: 1.5.234
node-releases: 2.0.23
update-browserslist-db: 1.1.3(browserslist@4.26.3)
+ browserslist@4.28.2:
+ dependencies:
+ baseline-browser-mapping: 2.10.30
+ caniuse-lite: 1.0.30001793
+ electron-to-chromium: 1.5.357
+ node-releases: 2.0.44
+ update-browserslist-db: 1.2.3(browserslist@4.28.2)
+
+ bser@2.1.1:
+ dependencies:
+ node-int64: 0.4.0
+
buffer-crc32@0.2.13: {}
buffer-crc32@1.0.0: {}
@@ -25774,12 +29765,18 @@ snapshots:
camelcase-css@2.0.1: {}
+ camelcase@5.3.1: {}
+
+ camelcase@6.3.0: {}
+
camelcase@8.0.0: {}
caniuse-lite@1.0.30001717: {}
caniuse-lite@1.0.30001750: {}
+ caniuse-lite@1.0.30001793: {}
+
canvas-confetti@1.9.3: {}
caseless@0.12.0: {}
@@ -25819,7 +29816,7 @@ snapshots:
assertion-error: 2.0.1
check-error: 2.1.1
deep-eql: 5.0.2
- loupe: 3.1.3
+ loupe: 3.2.1
pathval: 2.0.0
chalk@2.4.2:
@@ -25878,9 +29875,33 @@ snapshots:
chromatic@11.28.2: {}
+ chrome-launcher@0.15.2:
+ dependencies:
+ '@types/node': 20.19.21
+ escape-string-regexp: 4.0.0
+ is-wsl: 2.2.0
+ lighthouse-logger: 1.4.2
+ transitivePeerDependencies:
+ - supports-color
+
chrome-trace-event@1.0.4:
optional: true
+ chromium-edge-launcher@0.2.0:
+ dependencies:
+ '@types/node': 20.19.21
+ escape-string-regexp: 4.0.0
+ is-wsl: 2.2.0
+ lighthouse-logger: 1.4.2
+ mkdirp: 1.0.4
+ rimraf: 3.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ ci-info@2.0.0: {}
+
+ ci-info@3.9.0: {}
+
citty@0.1.6:
dependencies:
consola: 3.4.2
@@ -25901,6 +29922,10 @@ snapshots:
cli-boxes@3.0.0: {}
+ cli-cursor@2.1.0:
+ dependencies:
+ restore-cursor: 2.0.0
+
cli-cursor@5.0.0:
dependencies:
restore-cursor: 5.1.0
@@ -25925,8 +29950,7 @@ snapshots:
dependencies:
mimic-response: 1.0.1
- clone@1.0.4:
- optional: true
+ clone@1.0.4: {}
clsx@1.2.1: {}
@@ -25974,7 +29998,6 @@ snapshots:
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
- optional: true
colorspace@1.1.4:
dependencies:
@@ -25989,6 +30012,8 @@ snapshots:
commander@10.0.1: {}
+ commander@12.1.0: {}
+
commander@13.1.0: {}
commander@2.20.3: {}
@@ -25997,6 +30022,8 @@ snapshots:
commander@6.2.1: {}
+ commander@7.2.0: {}
+
commander@8.3.0: {}
common-ancestor-path@1.0.1: {}
@@ -26022,6 +30049,22 @@ snapshots:
normalize-path: 3.0.0
readable-stream: 4.7.0
+ compressible@2.0.18:
+ dependencies:
+ mime-db: 1.54.0
+
+ compression@1.8.1:
+ dependencies:
+ bytes: 3.1.2
+ compressible: 2.0.18
+ debug: 2.6.9
+ negotiator: 0.6.4
+ on-headers: 1.1.0
+ safe-buffer: 5.2.1
+ vary: 1.1.2
+ transitivePeerDependencies:
+ - supports-color
+
concat-map@0.0.1: {}
concat-stream@2.0.0:
@@ -26044,6 +30087,15 @@ snapshots:
confbox@0.2.2: {}
+ connect@3.7.0:
+ dependencies:
+ debug: 2.6.9
+ finalhandler: 1.1.2
+ parseurl: 1.3.3
+ utils-merge: 1.0.1
+ transitivePeerDependencies:
+ - supports-color
+
consola@3.4.2: {}
console-control-strings@1.1.0: {}
@@ -26079,6 +30131,10 @@ snapshots:
'@types/cookie': 0.6.0
cookie: 0.7.2
+ core-js-compat@3.49.0:
+ dependencies:
+ browserslist: 4.28.2
+
core-js@3.42.0: {}
core-util-is@1.0.3: {}
@@ -26137,6 +30193,25 @@ snapshots:
dependencies:
uncrypto: 0.1.3
+ css-in-js-utils@3.1.0:
+ dependencies:
+ hyphenate-style-name: 1.1.0
+
+ css-select@5.2.2:
+ dependencies:
+ boolbase: 1.0.0
+ css-what: 6.2.2
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ nth-check: 2.1.1
+
+ css-tree@1.1.3:
+ dependencies:
+ mdn-data: 2.0.14
+ source-map: 0.6.1
+
+ css-what@6.2.2: {}
+
css.escape@1.5.1: {}
cssesc@3.0.0: {}
@@ -26277,6 +30352,8 @@ snapshots:
dependencies:
character-entities: 2.0.2
+ decode-uri-component@0.2.2: {}
+
decompress-response@6.0.0:
dependencies:
mimic-response: 3.1.0
@@ -26320,7 +30397,6 @@ snapshots:
defaults@1.0.4:
dependencies:
clone: 1.0.4
- optional: true
defaults@2.0.2: {}
@@ -26405,10 +30481,10 @@ snapshots:
detective-typescript@11.2.0:
dependencies:
- '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.3)
+ '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.9.3)
ast-module-types: 5.0.0
node-source-walk: 6.0.2
- typescript: 5.8.3
+ typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -26439,6 +30515,8 @@ snapshots:
dependencies:
'@leichtgewicht/ip-codec': 2.0.5
+ dnssd-advertise@1.1.4: {}
+
doctrine@2.1.0:
dependencies:
esutils: 2.0.3
@@ -26567,6 +30645,8 @@ snapshots:
electron-to-chromium@1.5.234: {}
+ electron-to-chromium@1.5.357: {}
+
emoji-regex-xs@1.0.0: {}
emoji-regex@10.4.0: {}
@@ -26821,7 +30901,7 @@ snapshots:
esast-util-from-js@2.0.1:
dependencies:
'@types/estree-jsx': 1.0.5
- acorn: 8.15.0
+ acorn: 8.16.0
esast-util-from-estree: 2.0.0
vfile-message: 4.0.2
@@ -27063,6 +31143,8 @@ snapshots:
escape-string-regexp@1.0.5: {}
+ escape-string-regexp@2.0.0: {}
+
escape-string-regexp@4.0.0: {}
escape-string-regexp@5.0.0: {}
@@ -27075,20 +31157,20 @@ snapshots:
optionalDependencies:
source-map: 0.6.1
- eslint-config-next@13.3.0(eslint@8.57.1)(typescript@5.8.3):
+ eslint-config-next@13.3.0(eslint@8.57.1)(typescript@5.9.3):
dependencies:
'@next/eslint-plugin-next': 13.3.0
'@rushstack/eslint-patch': 1.11.0
- '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3)
+ '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1)
- eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.5(eslint@8.57.1)
eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1)
optionalDependencies:
- typescript: 5.8.3
+ typescript: 5.9.3
transitivePeerDependencies:
- eslint-import-resolver-webpack
- eslint-plugin-import-x
@@ -27142,7 +31224,7 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.7.2
optionalDependencies:
- eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
@@ -27161,11 +31243,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies:
debug: 3.2.7
optionalDependencies:
- '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3)
+ '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1)
@@ -27183,7 +31265,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
+ eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.8
@@ -27194,7 +31276,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -27206,7 +31288,7 @@ snapshots:
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
optionalDependencies:
- '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3)
+ '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.9.3)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
@@ -27346,11 +31428,11 @@ snapshots:
string.prototype.matchall: 4.0.12
string.prototype.repeat: 1.0.0
- eslint-plugin-tailwindcss@3.18.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.8.3))):
+ eslint-plugin-tailwindcss@3.18.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.9.3))):
dependencies:
fast-glob: 3.3.3
postcss: 8.5.3
- tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.8.3))
+ tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.9.3))
eslint-plugin-turbo@1.13.4(eslint@8.57.1):
dependencies:
@@ -27590,7 +31672,7 @@ snapshots:
estree-walker@3.0.3:
dependencies:
- '@types/estree': 1.0.7
+ '@types/estree': 1.0.8
esutils@2.0.3: {}
@@ -27652,6 +31734,273 @@ snapshots:
expect-type@1.2.1: {}
+ expo-asset@55.0.17(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3):
+ dependencies:
+ '@expo/image-utils': 0.8.14(typescript@5.9.3)
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-constants: 55.0.16(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ expo-clipboard@55.0.13(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+
+ expo-constants@55.0.16(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)):
+ dependencies:
+ '@expo/env': 2.1.2
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ expo-dev-client@55.0.34(expo@55.0.24):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-dev-launcher: 55.0.35(expo@55.0.24)
+ expo-dev-menu: 55.0.29(expo@55.0.24)
+ expo-dev-menu-interface: 55.0.2(expo@55.0.24)
+ expo-manifests: 55.0.17(expo@55.0.24)
+ expo-updates-interface: 55.1.6(expo@55.0.24)
+
+ expo-dev-launcher@55.0.35(expo@55.0.24):
+ dependencies:
+ '@expo/schema-utils': 55.0.4
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-dev-menu: 55.0.29(expo@55.0.24)
+ expo-manifests: 55.0.17(expo@55.0.24)
+
+ expo-dev-menu-interface@55.0.2(expo@55.0.24):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+
+ expo-dev-menu@55.0.29(expo@55.0.24):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-dev-menu-interface: 55.0.2(expo@55.0.24)
+
+ expo-document-picker@55.0.13(expo@55.0.24):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+
+ expo-file-system@55.0.20(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+
+ expo-font@55.0.7(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ fontfaceobserver: 2.3.0
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+
+ expo-glass-effect@55.0.11(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+
+ expo-image-loader@55.0.0(expo@55.0.24):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+
+ expo-image-picker@55.0.20(expo@55.0.24):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-image-loader: 55.0.0(expo@55.0.24)
+
+ expo-image@55.0.10(expo@55.0.24)(react-native-web@0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ sf-symbols-typescript: 2.2.0
+ optionalDependencies:
+ react-native-web: 0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+
+ expo-json-utils@55.0.2: {}
+
+ expo-keep-awake@55.0.8(expo@55.0.24)(react@19.2.0):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react: 19.2.0
+
+ expo-linking@55.0.15(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ expo-constants: 55.0.16(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))
+ invariant: 2.2.4
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ transitivePeerDependencies:
+ - expo
+ - supports-color
+
+ expo-manifests@55.0.17(expo@55.0.24):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-json-utils: 55.0.2
+
+ expo-media-library@55.0.17(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+
+ expo-modules-autolinking@55.0.22(typescript@5.9.3):
+ dependencies:
+ '@expo/require-utils': 55.0.5(typescript@5.9.3)
+ '@expo/spawn-async': 1.7.2
+ chalk: 4.1.2
+ commander: 7.2.0
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ expo-modules-core@55.0.25(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ invariant: 2.2.4
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ optionalDependencies:
+ react-native-worklets: 0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+
+ expo-router@55.0.14(eed5efedde241c317111b390e3f7dd2b):
+ dependencies:
+ '@expo/log-box': 55.0.12(@expo/dom-webview@55.0.6)(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@expo/metro-runtime': 55.0.11(@expo/dom-webview@55.0.6)(expo@55.0.24)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@expo/schema-utils': 55.0.4
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.0)
+ '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@react-navigation/bottom-tabs': 7.16.1(@react-navigation/native@7.2.4(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-screens@4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@react-navigation/native': 7.2.4(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@react-navigation/native-stack': 7.15.1(@react-navigation/native@7.2.4(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-screens@4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ client-only: 0.0.1
+ debug: 4.4.3(supports-color@8.1.1)
+ escape-string-regexp: 4.0.0
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-constants: 55.0.16(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))
+ expo-glass-effect: 55.0.11(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-image: 55.0.10(expo@55.0.24)(react-native-web@0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-linking: 55.0.15(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-server: 55.0.9
+ expo-symbols: 55.0.8(expo-font@55.0.7)(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ fast-deep-equal: 3.1.3
+ invariant: 2.2.4
+ nanoid: 3.3.11
+ query-string: 7.1.3
+ react: 19.2.0
+ react-fast-compare: 3.2.2
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ react-native-is-edge-to-edge: 1.3.1(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-safe-area-context: 5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-screens: 4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ semver: 7.6.3
+ server-only: 0.0.1
+ sf-symbols-typescript: 2.2.0
+ shallowequal: 1.1.0
+ use-latest-callback: 0.2.6(react@19.2.0)
+ vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ optionalDependencies:
+ '@testing-library/react-native': 13.3.3(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react-test-renderer@19.2.0(react@19.2.0))(react@19.2.0)
+ react-dom: 19.2.0(react@19.2.0)
+ react-native-gesture-handler: 2.30.1(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-reanimated: 4.2.1(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-web: 0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ transitivePeerDependencies:
+ - '@react-native-masked-view/masked-view'
+ - '@types/react'
+ - '@types/react-dom'
+ - expo-font
+ - supports-color
+
+ expo-secure-store@55.0.14(expo@55.0.24):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+
+ expo-server@55.0.9: {}
+
+ expo-sharing@55.0.19(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ '@expo/config-plugins': 55.0.9
+ '@expo/config-types': 55.0.5
+ '@expo/plist': 0.5.3
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ expo-symbols@55.0.8(expo-font@55.0.7)(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ '@expo-google-fonts/material-symbols': 0.4.38
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-font: 55.0.7(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ sf-symbols-typescript: 2.2.0
+
+ expo-updates-interface@55.1.6(expo@55.0.24):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+
+ expo-video@55.0.17(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+
+ expo-web-browser@55.0.16(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)):
+ dependencies:
+ expo: 55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+
+ expo@55.0.24(@babel/core@7.27.1)(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-router@55.0.14)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3):
+ dependencies:
+ '@babel/runtime': 7.27.1
+ '@expo/cli': 55.0.30(@expo/dom-webview@55.0.6)(@expo/metro-runtime@55.0.11)(expo-constants@55.0.16)(expo-font@55.0.7)(expo-router@55.0.14)(expo@55.0.24)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ '@expo/config': 55.0.17(typescript@5.9.3)
+ '@expo/config-plugins': 55.0.9
+ '@expo/devtools': 55.0.3(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@expo/fingerprint': 0.16.7
+ '@expo/local-build-cache-provider': 55.0.13(typescript@5.9.3)
+ '@expo/log-box': 55.0.12(@expo/dom-webview@55.0.6)(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@expo/metro': 55.1.1
+ '@expo/metro-config': 55.0.21(expo@55.0.24)(typescript@5.9.3)
+ '@expo/vector-icons': 15.1.1(expo-font@55.0.7)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@ungap/structured-clone': 1.3.0
+ babel-preset-expo: 55.0.21(@babel/core@7.27.1)(@babel/runtime@7.27.1)(expo@55.0.24)(react-refresh@0.14.2)
+ expo-asset: 55.0.17(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo-constants: 55.0.16(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))
+ expo-file-system: 55.0.20(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))
+ expo-font: 55.0.7(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ expo-keep-awake: 55.0.8(expo@55.0.24)(react@19.2.0)
+ expo-modules-autolinking: 55.0.22(typescript@5.9.3)
+ expo-modules-core: 55.0.25(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ pretty-format: 29.7.0
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ react-refresh: 0.14.2
+ whatwg-url-minimum: 0.1.2
+ optionalDependencies:
+ '@expo/dom-webview': 55.0.6(expo@55.0.24)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ '@expo/metro-runtime': 55.0.11(@expo/dom-webview@55.0.6)(expo@55.0.24)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ transitivePeerDependencies:
+ - '@babel/core'
+ - bufferutil
+ - expo-router
+ - expo-widgets
+ - react-dom
+ - react-native-worklets
+ - react-server-dom-webpack
+ - supports-color
+ - typescript
+ - utf-8-validate
+
exponential-backoff@3.1.2: {}
express-rate-limit@7.5.1(express@5.1.0):
@@ -27674,7 +32023,7 @@ snapshots:
etag: 1.8.1
finalhandler: 1.3.2
fresh: 0.5.2
- http-errors: 2.0.0
+ http-errors: 2.0.1
merge-descriptors: 1.0.3
methods: 1.1.2
on-finished: 2.4.1
@@ -27813,6 +32162,26 @@ snapshots:
dependencies:
reusify: 1.1.0
+ fb-dotslash@0.5.8: {}
+
+ fb-watchman@2.0.2:
+ dependencies:
+ bser: 2.1.1
+
+ fbjs-css-vars@1.0.2: {}
+
+ fbjs@3.0.5:
+ dependencies:
+ cross-fetch: 3.2.0(encoding@0.1.13)
+ fbjs-css-vars: 1.0.2
+ loose-envify: 1.4.0
+ object-assign: 4.1.1
+ promise: 7.3.1
+ setimmediate: 1.0.5
+ ua-parser-js: 1.0.41
+ transitivePeerDependencies:
+ - encoding
+
fd-slicer@1.1.0:
dependencies:
pend: 1.2.0
@@ -27840,6 +32209,8 @@ snapshots:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
+ fetch-nodeshim@0.4.10: {}
+
fflate@0.4.8: {}
fflate@0.8.2: {}
@@ -27899,8 +32270,22 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
+ filter-obj@1.1.0: {}
+
filter-obj@5.1.0: {}
+ finalhandler@1.1.2:
+ dependencies:
+ debug: 2.6.9
+ encodeurl: 1.0.2
+ escape-html: 1.0.3
+ on-finished: 2.3.0
+ parseurl: 1.3.3
+ statuses: 1.5.0
+ unpipe: 1.0.0
+ transitivePeerDependencies:
+ - supports-color
+
finalhandler@1.3.2:
dependencies:
debug: 2.6.9
@@ -27928,6 +32313,11 @@ snapshots:
find-up-simple@1.0.1: {}
+ find-up@4.1.0:
+ dependencies:
+ locate-path: 5.0.0
+ path-exists: 4.0.0
+
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
@@ -27950,7 +32340,7 @@ snapshots:
fix-dts-default-cjs-exports@1.0.1:
dependencies:
- magic-string: 0.30.19
+ magic-string: 0.30.21
mlly: 1.8.0
rollup: 4.40.2
@@ -27967,10 +32357,14 @@ snapshots:
flatted@3.3.3: {}
+ flow-enums-runtime@0.0.6: {}
+
fn.name@1.1.0: {}
follow-redirects@1.15.9: {}
+ fontfaceobserver@2.3.0: {}
+
for-each@0.3.5:
dependencies:
is-callable: 1.2.7
@@ -28074,9 +32468,9 @@ snapshots:
strip-ansi: 6.0.1
wide-align: 1.1.5
- geist@1.4.2(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)):
+ geist@1.4.2(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)):
dependencies:
- next: 16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ next: 16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
generate-function@2.3.1:
dependencies:
@@ -28144,6 +32538,8 @@ snapshots:
dependencies:
resolve-pkg-maps: 1.0.0
+ getenv@2.0.0: {}
+
gif.js@0.2.0: {}
giget@2.0.0:
@@ -28183,6 +32579,12 @@ snapshots:
package-json-from-dist: 1.0.1
path-scurry: 2.0.0
+ glob@13.0.6:
+ dependencies:
+ minimatch: 10.2.4
+ minipass: 7.1.3
+ path-scurry: 2.0.2
+
glob@7.1.7:
dependencies:
fs.realpath: 1.0.0
@@ -28239,7 +32641,7 @@ snapshots:
dependencies:
'@sindresorhus/merge-streams': 2.3.0
fast-glob: 3.3.3
- ignore: 7.0.4
+ ignore: 7.0.5
path-type: 6.0.0
slash: 5.1.0
unicorn-magic: 0.3.0
@@ -28453,12 +32855,40 @@ snapshots:
property-information: 7.0.0
space-separated-tokens: 2.0.2
+ hermes-compiler@0.14.1: {}
+
hermes-estree@0.25.1: {}
+ hermes-estree@0.32.0: {}
+
+ hermes-estree@0.32.1: {}
+
+ hermes-estree@0.33.3:
+ optional: true
+
+ hermes-estree@0.35.0: {}
+
hermes-parser@0.25.1:
dependencies:
hermes-estree: 0.25.1
+ hermes-parser@0.32.0:
+ dependencies:
+ hermes-estree: 0.32.0
+
+ hermes-parser@0.32.1:
+ dependencies:
+ hermes-estree: 0.32.1
+
+ hermes-parser@0.33.3:
+ dependencies:
+ hermes-estree: 0.33.3
+ optional: true
+
+ hermes-parser@0.35.0:
+ dependencies:
+ hermes-estree: 0.35.0
+
hls.js@0.14.17:
dependencies:
eventemitter3: 4.0.7
@@ -28468,6 +32898,10 @@ snapshots:
hls.js@1.6.2: {}
+ hoist-non-react-statics@3.3.2:
+ dependencies:
+ react-is: 16.13.1
+
hono@4.12.12: {}
hono@4.7.4: {}
@@ -28584,6 +33018,8 @@ snapshots:
dependencies:
ms: 2.1.3
+ hyphenate-style-name@1.1.0: {}
+
iconv-lite@0.4.24:
dependencies:
safer-buffer: 2.1.2
@@ -28608,10 +33044,12 @@ snapshots:
ignore@5.3.2: {}
- ignore@7.0.4: {}
-
ignore@7.0.5: {}
+ image-size@1.2.1:
+ dependencies:
+ queue: 6.0.2
+
immediate@3.0.6: {}
immer@10.2.0: {}
@@ -28649,6 +33087,10 @@ snapshots:
inline-style-parser@0.2.4: {}
+ inline-style-prefixer@7.0.1:
+ dependencies:
+ css-in-js-utils: 3.1.0
+
inspect-with-kind@1.0.5:
dependencies:
kind-of: 6.0.3
@@ -28663,6 +33105,10 @@ snapshots:
internmap@2.0.3: {}
+ invariant@2.2.4:
+ dependencies:
+ loose-envify: 1.4.0
+
ioredis@5.6.1:
dependencies:
'@ioredis/commands': 1.2.0
@@ -28686,7 +33132,7 @@ snapshots:
ipaddr.js@1.9.1: {}
- iron-session@6.3.1(express@5.1.0)(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)):
+ iron-session@6.3.1(express@5.1.0)(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)):
dependencies:
'@peculiar/webcrypto': 1.5.0
'@types/cookie': 0.5.4
@@ -28697,7 +33143,7 @@ snapshots:
iron-webcrypto: 0.2.8
optionalDependencies:
express: 5.1.0
- next: 16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ next: 16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
iron-webcrypto@0.2.8:
dependencies:
@@ -28922,6 +33368,16 @@ snapshots:
istanbul-lib-coverage@3.2.2: {}
+ istanbul-lib-instrument@5.2.1:
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/parser': 7.29.3
+ '@istanbuljs/schema': 0.1.3
+ istanbul-lib-coverage: 3.2.2
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
istanbul-lib-report@3.0.1:
dependencies:
istanbul-lib-coverage: 3.2.2
@@ -28968,6 +33424,85 @@ snapshots:
filelist: 1.0.4
picocolors: 1.1.1
+ jest-diff@30.4.1:
+ dependencies:
+ '@jest/diff-sequences': 30.4.0
+ '@jest/get-type': 30.1.0
+ chalk: 4.1.2
+ pretty-format: 30.4.1
+
+ jest-environment-node@29.7.0:
+ dependencies:
+ '@jest/environment': 29.7.0
+ '@jest/fake-timers': 29.7.0
+ '@jest/types': 29.6.3
+ '@types/node': 20.19.21
+ jest-mock: 29.7.0
+ jest-util: 29.7.0
+
+ jest-get-type@29.6.3: {}
+
+ jest-haste-map@29.7.0:
+ dependencies:
+ '@jest/types': 29.6.3
+ '@types/graceful-fs': 4.1.9
+ '@types/node': 20.19.21
+ anymatch: 3.1.3
+ fb-watchman: 2.0.2
+ graceful-fs: 4.2.11
+ jest-regex-util: 29.6.3
+ jest-util: 29.7.0
+ jest-worker: 29.7.0
+ micromatch: 4.0.8
+ walker: 1.0.8
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ jest-matcher-utils@30.4.1:
+ dependencies:
+ '@jest/get-type': 30.1.0
+ chalk: 4.1.2
+ jest-diff: 30.4.1
+ pretty-format: 30.4.1
+
+ jest-message-util@29.7.0:
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@jest/types': 29.6.3
+ '@types/stack-utils': 2.0.3
+ chalk: 4.1.2
+ graceful-fs: 4.2.11
+ micromatch: 4.0.8
+ pretty-format: 29.7.0
+ slash: 3.0.0
+ stack-utils: 2.0.6
+
+ jest-mock@29.7.0:
+ dependencies:
+ '@jest/types': 29.6.3
+ '@types/node': 20.19.21
+ jest-util: 29.7.0
+
+ jest-regex-util@29.6.3: {}
+
+ jest-util@29.7.0:
+ dependencies:
+ '@jest/types': 29.6.3
+ '@types/node': 20.19.21
+ chalk: 4.1.2
+ ci-info: 3.9.0
+ graceful-fs: 4.2.11
+ picomatch: 2.3.1
+
+ jest-validate@29.7.0:
+ dependencies:
+ '@jest/types': 29.6.3
+ camelcase: 6.3.0
+ chalk: 4.1.2
+ jest-get-type: 29.6.3
+ leven: 3.1.0
+ pretty-format: 29.7.0
+
jest-worker@27.5.1:
dependencies:
'@types/node': 20.19.21
@@ -28975,6 +33510,15 @@ snapshots:
supports-color: 8.1.1
optional: true
+ jest-worker@29.7.0:
+ dependencies:
+ '@types/node': 20.19.21
+ jest-util: 29.7.0
+ merge-stream: 2.0.0
+ supports-color: 8.1.1
+
+ jimp-compact@0.16.1: {}
+
jiti@1.21.7: {}
jiti@2.4.2: {}
@@ -29010,6 +33554,8 @@ snapshots:
jsbn@1.1.0: {}
+ jsc-safe-url@0.2.4: {}
+
jsdoc-type-pratt-parser@4.1.0: {}
jsdom@26.1.0:
@@ -29116,6 +33662,8 @@ snapshots:
dotenv: 16.5.0
winston: 3.17.0
+ lan-network@0.2.1: {}
+
language-subtag-registry@0.3.23: {}
language-tags@1.0.9:
@@ -29130,6 +33678,8 @@ snapshots:
leb@1.0.0: {}
+ leven@3.1.0: {}
+
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
@@ -29139,6 +33689,62 @@ snapshots:
dependencies:
immediate: 3.0.6
+ lighthouse-logger@1.4.2:
+ dependencies:
+ debug: 2.6.9
+ marky: 1.3.0
+ transitivePeerDependencies:
+ - supports-color
+
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.32.0
+ lightningcss-darwin-arm64: 1.32.0
+ lightningcss-darwin-x64: 1.32.0
+ lightningcss-freebsd-x64: 1.32.0
+ lightningcss-linux-arm-gnueabihf: 1.32.0
+ lightningcss-linux-arm64-gnu: 1.32.0
+ lightningcss-linux-arm64-musl: 1.32.0
+ lightningcss-linux-x64-gnu: 1.32.0
+ lightningcss-linux-x64-musl: 1.32.0
+ lightningcss-win32-arm64-msvc: 1.32.0
+ lightningcss-win32-x64-msvc: 1.32.0
+
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
@@ -29182,6 +33788,10 @@ snapshots:
pkg-types: 2.1.0
quansync: 0.2.10
+ locate-path@5.0.0:
+ dependencies:
+ p-locate: 4.1.0
+
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
@@ -29212,12 +33822,18 @@ snapshots:
lodash.sortby@4.7.0: {}
+ lodash.throttle@4.1.1: {}
+
lodash.truncate@4.4.2: {}
lodash.union@4.6.0: {}
lodash@4.17.21: {}
+ log-symbols@2.2.0:
+ dependencies:
+ chalk: 2.4.2
+
log-symbols@6.0.0:
dependencies:
chalk: 5.6.2
@@ -29301,8 +33917,8 @@ snapshots:
magicast@0.2.11:
dependencies:
- '@babel/parser': 7.27.5
- '@babel/types': 7.27.1
+ '@babel/parser': 7.29.3
+ '@babel/types': 7.29.0
recast: 0.23.11
magicast@0.3.5:
@@ -29317,7 +33933,7 @@ snapshots:
make-dir@4.0.0:
dependencies:
- semver: 7.7.3
+ semver: 7.7.4
make-error@1.3.6:
optional: true
@@ -29339,6 +33955,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ makeerror@1.0.12:
+ dependencies:
+ tmpl: 1.0.5
+
map-or-similar@1.5.0: {}
markdown-extensions@2.0.0: {}
@@ -29347,6 +33967,8 @@ snapshots:
marked@7.0.4: {}
+ marky@1.3.0: {}
+
math-intrinsics@1.1.0: {}
md-to-react-email@5.0.5(react@19.1.1):
@@ -29522,6 +34144,8 @@ snapshots:
dependencies:
'@types/mdast': 4.0.4
+ mdn-data@2.0.14: {}
+
media-chrome@4.12.0(react@19.2.4):
dependencies:
ce-la-react: 0.3.0(react@19.2.4)
@@ -29546,6 +34170,10 @@ snapshots:
'@types/dom-mediacapture-transform': 0.1.11
'@types/dom-webcodecs': 0.1.13
+ memoize-one@5.2.1: {}
+
+ memoize-one@6.0.0: {}
+
memoizerific@1.11.3:
dependencies:
map-or-similar: 1.5.0
@@ -29568,6 +34196,368 @@ snapshots:
methods@1.1.2: {}
+ metro-babel-transformer@0.83.7:
+ dependencies:
+ '@babel/core': 7.27.1
+ flow-enums-runtime: 0.0.6
+ hermes-parser: 0.35.0
+ metro-cache-key: 0.83.7
+ nullthrows: 1.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ metro-babel-transformer@0.84.4:
+ dependencies:
+ '@babel/core': 7.27.1
+ flow-enums-runtime: 0.0.6
+ hermes-parser: 0.35.0
+ metro-cache-key: 0.84.4
+ nullthrows: 1.1.1
+ transitivePeerDependencies:
+ - supports-color
+ optional: true
+
+ metro-cache-key@0.83.7:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+
+ metro-cache-key@0.84.4:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+ optional: true
+
+ metro-cache@0.83.7:
+ dependencies:
+ exponential-backoff: 3.1.2
+ flow-enums-runtime: 0.0.6
+ https-proxy-agent: 7.0.6
+ metro-core: 0.83.7
+ transitivePeerDependencies:
+ - supports-color
+
+ metro-cache@0.84.4:
+ dependencies:
+ exponential-backoff: 3.1.2
+ flow-enums-runtime: 0.0.6
+ https-proxy-agent: 7.0.6
+ metro-core: 0.84.4
+ transitivePeerDependencies:
+ - supports-color
+ optional: true
+
+ metro-config@0.83.7:
+ dependencies:
+ connect: 3.7.0
+ flow-enums-runtime: 0.0.6
+ jest-validate: 29.7.0
+ metro: 0.83.7
+ metro-cache: 0.83.7
+ metro-core: 0.83.7
+ metro-runtime: 0.83.7
+ yaml: 2.8.1
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ metro-config@0.84.4:
+ dependencies:
+ connect: 3.7.0
+ flow-enums-runtime: 0.0.6
+ jest-validate: 29.7.0
+ metro: 0.84.4
+ metro-cache: 0.84.4
+ metro-core: 0.84.4
+ metro-runtime: 0.84.4
+ yaml: 2.8.1
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ optional: true
+
+ metro-core@0.83.7:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+ lodash.throttle: 4.1.1
+ metro-resolver: 0.83.7
+
+ metro-core@0.84.4:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+ lodash.throttle: 4.1.1
+ metro-resolver: 0.84.4
+ optional: true
+
+ metro-file-map@0.83.7:
+ dependencies:
+ debug: 4.4.3(supports-color@8.1.1)
+ fb-watchman: 2.0.2
+ flow-enums-runtime: 0.0.6
+ graceful-fs: 4.2.11
+ invariant: 2.2.4
+ jest-worker: 29.7.0
+ micromatch: 4.0.8
+ nullthrows: 1.1.1
+ walker: 1.0.8
+ transitivePeerDependencies:
+ - supports-color
+
+ metro-file-map@0.84.4:
+ dependencies:
+ debug: 4.4.3(supports-color@8.1.1)
+ fb-watchman: 2.0.2
+ flow-enums-runtime: 0.0.6
+ graceful-fs: 4.2.11
+ invariant: 2.2.4
+ jest-worker: 29.7.0
+ micromatch: 4.0.8
+ nullthrows: 1.1.1
+ walker: 1.0.8
+ transitivePeerDependencies:
+ - supports-color
+ optional: true
+
+ metro-minify-terser@0.83.7:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+ terser: 5.44.0
+
+ metro-minify-terser@0.84.4:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+ terser: 5.44.0
+ optional: true
+
+ metro-resolver@0.83.7:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+
+ metro-resolver@0.84.4:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+ optional: true
+
+ metro-runtime@0.83.7:
+ dependencies:
+ '@babel/runtime': 7.27.1
+ flow-enums-runtime: 0.0.6
+
+ metro-runtime@0.84.4:
+ dependencies:
+ '@babel/runtime': 7.27.1
+ flow-enums-runtime: 0.0.6
+ optional: true
+
+ metro-source-map@0.83.7:
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ flow-enums-runtime: 0.0.6
+ invariant: 2.2.4
+ metro-symbolicate: 0.83.7
+ nullthrows: 1.1.1
+ ob1: 0.83.7
+ source-map: 0.5.7
+ vlq: 1.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ metro-source-map@0.84.4:
+ dependencies:
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ flow-enums-runtime: 0.0.6
+ invariant: 2.2.4
+ metro-symbolicate: 0.84.4
+ nullthrows: 1.1.1
+ ob1: 0.84.4
+ source-map: 0.5.7
+ vlq: 1.0.1
+ transitivePeerDependencies:
+ - supports-color
+ optional: true
+
+ metro-symbolicate@0.83.7:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+ invariant: 2.2.4
+ metro-source-map: 0.83.7
+ nullthrows: 1.1.1
+ source-map: 0.5.7
+ vlq: 1.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ metro-symbolicate@0.84.4:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+ invariant: 2.2.4
+ metro-source-map: 0.84.4
+ nullthrows: 1.1.1
+ source-map: 0.5.7
+ vlq: 1.0.1
+ transitivePeerDependencies:
+ - supports-color
+ optional: true
+
+ metro-transform-plugins@0.83.7:
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/generator': 7.29.1
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ flow-enums-runtime: 0.0.6
+ nullthrows: 1.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ metro-transform-plugins@0.84.4:
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/generator': 7.29.1
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ flow-enums-runtime: 0.0.6
+ nullthrows: 1.1.1
+ transitivePeerDependencies:
+ - supports-color
+ optional: true
+
+ metro-transform-worker@0.83.7:
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/generator': 7.29.1
+ '@babel/parser': 7.29.3
+ '@babel/types': 7.29.0
+ flow-enums-runtime: 0.0.6
+ metro: 0.83.7
+ metro-babel-transformer: 0.83.7
+ metro-cache: 0.83.7
+ metro-cache-key: 0.83.7
+ metro-minify-terser: 0.83.7
+ metro-source-map: 0.83.7
+ metro-transform-plugins: 0.83.7
+ nullthrows: 1.1.1
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ metro-transform-worker@0.84.4:
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/generator': 7.29.1
+ '@babel/parser': 7.29.3
+ '@babel/types': 7.29.0
+ flow-enums-runtime: 0.0.6
+ metro: 0.84.4
+ metro-babel-transformer: 0.84.4
+ metro-cache: 0.84.4
+ metro-cache-key: 0.84.4
+ metro-minify-terser: 0.84.4
+ metro-source-map: 0.84.4
+ metro-transform-plugins: 0.84.4
+ nullthrows: 1.1.1
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ optional: true
+
+ metro@0.83.7:
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/core': 7.27.1
+ '@babel/generator': 7.29.1
+ '@babel/parser': 7.29.3
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ accepts: 2.0.0
+ ci-info: 2.0.0
+ connect: 3.7.0
+ debug: 4.4.3(supports-color@8.1.1)
+ error-stack-parser: 2.1.4
+ flow-enums-runtime: 0.0.6
+ graceful-fs: 4.2.11
+ hermes-parser: 0.35.0
+ image-size: 1.2.1
+ invariant: 2.2.4
+ jest-worker: 29.7.0
+ jsc-safe-url: 0.2.4
+ lodash.throttle: 4.1.1
+ metro-babel-transformer: 0.83.7
+ metro-cache: 0.83.7
+ metro-cache-key: 0.83.7
+ metro-config: 0.83.7
+ metro-core: 0.83.7
+ metro-file-map: 0.83.7
+ metro-resolver: 0.83.7
+ metro-runtime: 0.83.7
+ metro-source-map: 0.83.7
+ metro-symbolicate: 0.83.7
+ metro-transform-plugins: 0.83.7
+ metro-transform-worker: 0.83.7
+ mime-types: 3.0.1
+ nullthrows: 1.1.1
+ serialize-error: 2.1.0
+ source-map: 0.5.7
+ throat: 5.0.0
+ ws: 7.5.10
+ yargs: 17.7.2
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
+ metro@0.84.4:
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/core': 7.27.1
+ '@babel/generator': 7.29.1
+ '@babel/parser': 7.29.3
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
+ accepts: 2.0.0
+ ci-info: 2.0.0
+ connect: 3.7.0
+ debug: 4.4.3(supports-color@8.1.1)
+ error-stack-parser: 2.1.4
+ flow-enums-runtime: 0.0.6
+ graceful-fs: 4.2.11
+ hermes-parser: 0.35.0
+ image-size: 1.2.1
+ invariant: 2.2.4
+ jest-worker: 29.7.0
+ jsc-safe-url: 0.2.4
+ lodash.throttle: 4.1.1
+ metro-babel-transformer: 0.84.4
+ metro-cache: 0.84.4
+ metro-cache-key: 0.84.4
+ metro-config: 0.84.4
+ metro-core: 0.84.4
+ metro-file-map: 0.84.4
+ metro-resolver: 0.84.4
+ metro-runtime: 0.84.4
+ metro-source-map: 0.84.4
+ metro-symbolicate: 0.84.4
+ metro-transform-plugins: 0.84.4
+ metro-transform-worker: 0.84.4
+ mime-types: 3.0.1
+ nullthrows: 1.1.1
+ serialize-error: 2.1.0
+ source-map: 0.5.7
+ throat: 5.0.0
+ ws: 7.5.10
+ yargs: 17.7.2
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ optional: true
+
micro-api-client@3.3.0: {}
micromark-core-commonmark@2.0.3:
@@ -29857,6 +34847,8 @@ snapshots:
mime@4.0.7: {}
+ mimic-fn@1.2.0: {}
+
mimic-fn@2.1.0: {}
mimic-fn@4.0.0: {}
@@ -29959,6 +34951,8 @@ snapshots:
minipass@7.1.2: {}
+ minipass@7.1.3: {}
+
minizlib@2.1.2:
dependencies:
minipass: 3.3.6
@@ -30057,6 +35051,8 @@ snapshots:
multipasta@0.2.7: {}
+ multitars@1.0.0: {}
+
mustache@4.2.0: {}
mux-embed@5.9.0: {}
@@ -30115,13 +35111,13 @@ snapshots:
p-wait-for: 5.0.2
qs: 6.14.0
- next-auth@4.24.11(next@15.5.9(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
+ next-auth@4.24.11(next@15.5.9(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
dependencies:
'@babel/runtime': 7.27.1
'@panva/hkdf': 1.2.1
cookie: 0.7.2
jose: 4.15.9
- next: 15.5.9(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ next: 15.5.9(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
oauth: 0.9.15
openid-client: 5.7.1
preact: 10.26.6
@@ -30132,13 +35128,13 @@ snapshots:
optionalDependencies:
nodemailer: 6.10.1
- next-auth@4.24.11(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@6.10.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ next-auth@4.24.11(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@6.10.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@babel/runtime': 7.27.1
'@panva/hkdf': 1.2.1
cookie: 0.7.2
jose: 4.15.9
- next: 16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ next: 16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
oauth: 0.9.15
openid-client: 5.7.1
preact: 10.26.6
@@ -30164,7 +35160,7 @@ snapshots:
- acorn
- supports-color
- next@15.5.9(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
+ next@15.5.9(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
dependencies:
'@next/env': 15.5.9
'@swc/helpers': 0.5.15
@@ -30183,12 +35179,13 @@ snapshots:
'@next/swc-win32-arm64-msvc': 15.5.7
'@next/swc-win32-x64-msvc': 15.5.7
'@opentelemetry/api': 1.9.0
+ babel-plugin-react-compiler: 1.0.0
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
- next@15.5.9(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ next@15.5.9(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@next/env': 15.5.9
'@swc/helpers': 0.5.15
@@ -30207,12 +35204,13 @@ snapshots:
'@next/swc-win32-arm64-msvc': 15.5.7
'@next/swc-win32-x64-msvc': 15.5.7
'@opentelemetry/api': 1.9.0
+ babel-plugin-react-compiler: 1.0.0
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
- next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@next/env': 16.2.1
'@swc/helpers': 0.5.15
@@ -30232,6 +35230,7 @@ snapshots:
'@next/swc-win32-arm64-msvc': 16.2.1
'@next/swc-win32-x64-msvc': 16.2.1
'@opentelemetry/api': 1.9.0
+ babel-plugin-react-compiler: 1.0.0
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
@@ -30277,7 +35276,7 @@ snapshots:
klona: 2.0.6
knitwork: 1.2.0
listhen: 1.9.0
- magic-string: 0.30.17
+ magic-string: 0.30.21
magicast: 0.3.5
mime: 4.0.7
mlly: 1.7.4
@@ -30293,7 +35292,7 @@ snapshots:
rollup: 4.40.2
rollup-plugin-visualizer: 5.14.0(rolldown@1.0.1)(rollup@4.40.2)
scule: 1.3.0
- semver: 7.7.2
+ semver: 7.7.4
serve-placeholder: 2.0.2
serve-static: 2.2.0
source-map: 0.7.4
@@ -30375,6 +35374,8 @@ snapshots:
node-forge@1.3.1: {}
+ node-forge@1.4.0: {}
+
node-gyp-build-optional-packages@5.1.1:
dependencies:
detect-libc: 2.1.2
@@ -30410,9 +35411,11 @@ snapshots:
node-releases@2.0.23: {}
+ node-releases@2.0.44: {}
+
node-source-walk@6.0.2:
dependencies:
- '@babel/parser': 7.28.4
+ '@babel/parser': 7.29.3
nodemailer@6.10.1: {}
@@ -30431,7 +35434,7 @@ snapshots:
normalize-package-data@6.0.2:
dependencies:
hosted-git-info: 7.0.2
- semver: 7.7.2
+ semver: 7.7.4
validate-npm-package-license: 3.0.4
normalize-path@2.1.1:
@@ -30452,7 +35455,7 @@ snapshots:
npm-install-checks@6.3.0:
dependencies:
- semver: 7.7.3
+ semver: 7.7.4
npm-normalize-package-bin@3.0.1: {}
@@ -30460,7 +35463,7 @@ snapshots:
dependencies:
hosted-git-info: 7.0.2
proc-log: 4.2.0
- semver: 7.7.3
+ semver: 7.7.4
validate-npm-package-name: 5.0.1
npm-packlist@8.0.2:
@@ -30472,7 +35475,7 @@ snapshots:
npm-install-checks: 6.3.0
npm-normalize-package-bin: 3.0.1
npm-package-arg: 11.0.3
- semver: 7.7.3
+ semver: 7.7.4
npm-registry-fetch@17.1.0:
dependencies:
@@ -30502,6 +35505,12 @@ snapshots:
gauge: 3.0.2
set-blocking: 2.0.0
+ nth-check@2.1.1:
+ dependencies:
+ boolbase: 1.0.0
+
+ nullthrows@1.1.1: {}
+
number-flow@0.5.7:
dependencies:
esm-env: 1.2.2
@@ -30518,6 +35527,15 @@ snapshots:
oauth@0.9.15: {}
+ ob1@0.83.7:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+
+ ob1@0.84.4:
+ dependencies:
+ flow-enums-runtime: 0.0.6
+ optional: true
+
object-assign@4.1.1: {}
object-hash@2.2.0: {}
@@ -30596,10 +35614,16 @@ snapshots:
oidc-token-hash@5.1.1: {}
+ on-finished@2.3.0:
+ dependencies:
+ ee-first: 1.1.1
+
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
+ on-headers@1.1.0: {}
+
once@1.4.0:
dependencies:
wrappy: 1.0.2
@@ -30608,6 +35632,10 @@ snapshots:
dependencies:
fn.name: 1.1.0
+ onetime@2.0.1:
+ dependencies:
+ mimic-fn: 1.2.0
+
onetime@5.1.2:
dependencies:
mimic-fn: 2.1.0
@@ -30641,6 +35669,11 @@ snapshots:
is-inside-container: 1.0.0
wsl-utils: 0.1.0
+ open@7.4.2:
+ dependencies:
+ is-docker: 2.2.1
+ is-wsl: 2.2.0
+
open@8.4.0:
dependencies:
define-lazy-prop: 2.0.0
@@ -30686,9 +35719,18 @@ snapshots:
type-check: 0.4.0
word-wrap: 1.2.5
+ ora@3.4.0:
+ dependencies:
+ chalk: 2.4.2
+ cli-cursor: 2.1.0
+ cli-spinners: 2.9.2
+ log-symbols: 2.2.0
+ strip-ansi: 5.2.0
+ wcwidth: 1.0.1
+
ora@8.2.0:
dependencies:
- chalk: 5.4.1
+ chalk: 5.6.2
cli-cursor: 5.0.0
cli-spinners: 2.9.2
is-interactive: 2.0.0
@@ -30714,6 +35756,10 @@ snapshots:
dependencies:
p-timeout: 5.1.0
+ p-limit@2.3.0:
+ dependencies:
+ p-try: 2.2.0
+
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
@@ -30722,6 +35768,10 @@ snapshots:
dependencies:
yocto-queue: 1.2.1
+ p-locate@4.1.0:
+ dependencies:
+ p-limit: 2.3.0
+
p-locate@5.0.0:
dependencies:
p-limit: 3.1.0
@@ -30740,6 +35790,8 @@ snapshots:
p-timeout@6.1.4: {}
+ p-try@2.2.0: {}
+
p-wait-for@5.0.2:
dependencies:
p-timeout: 6.1.4
@@ -30807,12 +35859,16 @@ snapshots:
parse-json@8.3.0:
dependencies:
- '@babel/code-frame': 7.27.1
+ '@babel/code-frame': 7.29.0
index-to-position: 1.1.0
type-fest: 4.41.0
parse-numeric-range@1.3.0: {}
+ parse-png@2.1.0:
+ dependencies:
+ pngjs: 3.4.0
+
parse5@7.3.0:
dependencies:
entities: 6.0.0
@@ -30848,6 +35904,11 @@ snapshots:
lru-cache: 11.1.0
minipass: 7.1.2
+ path-scurry@2.0.2:
+ dependencies:
+ lru-cache: 11.1.0
+ minipass: 7.1.3
+
path-to-regexp@0.1.13: {}
path-to-regexp@6.3.0: {}
@@ -30920,8 +35981,16 @@ snapshots:
transitivePeerDependencies:
- react
+ plist@3.1.1:
+ dependencies:
+ '@xmldom/xmldom': 0.9.10
+ base64-js: 1.5.1
+ xmlbuilder: 15.1.1
+
pluralize@8.0.0: {}
+ pngjs@3.4.0: {}
+
polished@4.3.1:
dependencies:
'@babel/runtime': 7.27.1
@@ -30956,13 +36025,21 @@ snapshots:
postcss: 8.5.3
ts-node: 10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.8.3)
- postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3)):
+ postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.9.3)):
+ dependencies:
+ lilconfig: 3.1.3
+ yaml: 2.7.1
+ optionalDependencies:
+ postcss: 8.5.3
+ ts-node: 10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.9.3)
+
+ postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3)):
dependencies:
lilconfig: 3.1.3
yaml: 2.7.1
optionalDependencies:
postcss: 8.5.3
- ts-node: 10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3)
+ ts-node: 10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3)
postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.3)(yaml@2.8.1):
dependencies:
@@ -31006,6 +36083,12 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
+ postcss@8.4.49:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
postcss@8.5.3:
dependencies:
nanoid: 3.3.11
@@ -31065,8 +36148,21 @@ snapshots:
ansi-styles: 5.2.0
react-is: 17.0.2
+ pretty-format@29.7.0:
+ dependencies:
+ '@jest/schemas': 29.6.3
+ ansi-styles: 5.2.0
+ react-is: 18.3.1
+
pretty-format@3.8.0: {}
+ pretty-format@30.4.1:
+ dependencies:
+ '@jest/schemas': 30.4.1
+ ansi-styles: 5.2.0
+ react-is-18: react-is@18.3.1
+ react-is-19: react-is@19.2.6
+
printable-characters@1.0.42: {}
prismjs@1.30.0: {}
@@ -31092,6 +36188,14 @@ snapshots:
err-code: 2.0.3
retry: 0.12.0
+ promise@7.3.1:
+ dependencies:
+ asap: 2.0.6
+
+ promise@8.3.0:
+ dependencies:
+ asap: 2.0.6
+
prompts@2.4.2:
dependencies:
kleur: 3.0.3
@@ -31154,10 +36258,21 @@ snapshots:
quansync@0.2.11: {}
+ query-string@7.1.3:
+ dependencies:
+ decode-uri-component: 0.2.2
+ filter-obj: 1.1.0
+ split-on-first: 1.1.0
+ strict-uri-encode: 2.0.0
+
querystring@0.2.0: {}
queue-microtask@1.2.3: {}
+ queue@6.0.2:
+ dependencies:
+ inherits: 2.0.4
+
quick-lru@5.1.1: {}
quote-unquote@1.0.0: {}
@@ -31204,11 +36319,24 @@ snapshots:
react: 19.2.4
tween-functions: 1.2.0
+ react-devtools-core@6.1.5:
+ dependencies:
+ shell-quote: 1.8.3
+ ws: 7.5.10
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
react-dom@19.1.1(react@19.1.1):
dependencies:
react: 19.1.1
scheduler: 0.26.0
+ react-dom@19.2.0(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ scheduler: 0.27.0
+
react-dom@19.2.4(react@19.2.4):
dependencies:
react: 19.2.4
@@ -31221,7 +36349,7 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
- react-email@4.0.16(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
+ react-email@4.0.16(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
dependencies:
'@babel/parser': 7.27.5
'@babel/traverse': 7.27.4
@@ -31233,7 +36361,7 @@ snapshots:
glob: 11.0.3
log-symbols: 7.0.1
mime-types: 3.0.1
- next: 15.5.9(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ next: 15.5.9(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
normalize-path: 3.0.0
ora: 8.2.0
socket.io: 4.8.1
@@ -31273,6 +36401,12 @@ snapshots:
- supports-color
- utf-8-validate
+ react-fast-compare@3.2.2: {}
+
+ react-freeze@1.0.4(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+
react-hls-player@3.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
hls.js: 0.14.17
@@ -31300,6 +36434,10 @@ snapshots:
react-is@17.0.2: {}
+ react-is@18.3.1: {}
+
+ react-is@19.2.6: {}
+
react-loading-skeleton@3.5.0(react@19.2.4):
dependencies:
react: 19.2.4
@@ -31322,6 +36460,133 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ react-native-gesture-handler@2.30.1(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ '@egjs/hammerjs': 2.0.17
+ hoist-non-react-statics: 3.3.2
+ invariant: 2.2.4
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+
+ react-native-is-edge-to-edge@1.2.1(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+
+ react-native-is-edge-to-edge@1.3.1(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+
+ react-native-reanimated@4.2.1(react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ react-native-is-edge-to-edge: 1.2.1(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ react-native-worklets: 0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ semver: 7.7.3
+
+ react-native-safe-area-context@5.6.2(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+
+ react-native-screens@4.23.0(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-freeze: 1.0.4(react@19.2.0)
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ warn-once: 0.1.1
+
+ react-native-svg@15.15.5(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ css-select: 5.2.2
+ css-tree: 1.1.3
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+
+ react-native-web@0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ dependencies:
+ '@babel/runtime': 7.27.1
+ '@react-native/normalize-colors': 0.74.89
+ fbjs: 3.0.5
+ inline-style-prefixer: 7.0.1
+ memoize-one: 6.0.0
+ nullthrows: 1.1.1
+ postcss-value-parser: 4.2.0
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ styleq: 0.1.3
+ transitivePeerDependencies:
+ - encoding
+
+ react-native-worklets@0.7.4(@babel/core@7.27.1)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
+ dependencies:
+ '@babel/core': 7.27.1
+ '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.27.1)
+ '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.27.1)
+ '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.27.1)
+ '@babel/preset-typescript': 7.27.1(@babel/core@7.27.1)
+ convert-source-map: 2.0.0
+ react: 19.2.0
+ react-native: 0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0)
+ semver: 7.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0):
+ dependencies:
+ '@jest/create-cache-key-function': 29.7.0
+ '@react-native/assets-registry': 0.83.6
+ '@react-native/codegen': 0.83.6(@babel/core@7.27.1)
+ '@react-native/community-cli-plugin': 0.83.6(@react-native/metro-config@0.85.3(@babel/core@7.27.1))
+ '@react-native/gradle-plugin': 0.83.6
+ '@react-native/js-polyfills': 0.83.6
+ '@react-native/normalize-colors': 0.83.6
+ '@react-native/virtualized-lists': 0.83.6(@types/react@19.2.14)(react-native@0.83.6(@babel/core@7.27.1)(@react-native/metro-config@0.85.3(@babel/core@7.27.1))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
+ abort-controller: 3.0.0
+ anser: 1.4.10
+ ansi-regex: 5.0.1
+ babel-jest: 29.7.0(@babel/core@7.27.1)
+ babel-plugin-syntax-hermes-parser: 0.32.0
+ base64-js: 1.5.1
+ commander: 12.1.0
+ flow-enums-runtime: 0.0.6
+ glob: 7.2.3
+ hermes-compiler: 0.14.1
+ invariant: 2.2.4
+ jest-environment-node: 29.7.0
+ memoize-one: 5.2.1
+ metro-runtime: 0.83.7
+ metro-source-map: 0.83.7
+ nullthrows: 1.1.1
+ pretty-format: 29.7.0
+ promise: 8.3.0
+ react: 19.2.0
+ react-devtools-core: 6.1.5
+ react-refresh: 0.14.2
+ regenerator-runtime: 0.13.11
+ scheduler: 0.27.0
+ semver: 7.7.4
+ stacktrace-parser: 0.1.11
+ whatwg-fetch: 3.6.20
+ ws: 7.5.10
+ yargs: 17.7.2
+ optionalDependencies:
+ '@types/react': 19.2.14
+ transitivePeerDependencies:
+ - '@babel/core'
+ - '@react-native-community/cli'
+ - '@react-native/metro-config'
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
react-promise-suspense@0.3.4:
dependencies:
fast-deep-equal: 2.0.1
@@ -31335,8 +36600,18 @@ snapshots:
'@types/react': 19.2.14
redux: 5.0.1
+ react-refresh@0.14.2: {}
+
react-refresh@0.17.0: {}
+ react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.0)
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4):
dependencies:
react: 19.2.4
@@ -31356,6 +36631,17 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
+ react-remove-scroll@2.6.3(@types/react@19.2.14)(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.0)
+ react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.0)
+ tslib: 2.8.1
+ use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.0)
+ use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.14
+
react-remove-scroll@2.6.3(@types/react@19.2.14)(react@19.2.4):
dependencies:
react: 19.2.4
@@ -31405,6 +36691,14 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
+ react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.0):
+ dependencies:
+ get-nonce: 1.0.1
+ react: 19.2.0
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4):
dependencies:
get-nonce: 1.0.1
@@ -31413,6 +36707,12 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
+ react-test-renderer@19.2.0(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-is: 19.2.6
+ scheduler: 0.27.0
+
react-tooltip@5.28.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@floating-ui/dom': 1.7.0
@@ -31422,6 +36722,8 @@ snapshots:
react@19.1.1: {}
+ react@19.2.0: {}
+
react@19.2.4: {}
read-cache@1.0.0:
@@ -31493,7 +36795,7 @@ snapshots:
tiny-invariant: 1.3.3
tslib: 2.8.1
- recharts@3.4.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@17.0.2)(react@19.2.4)(redux@5.0.1):
+ recharts@3.4.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@19.2.6)(react@19.2.4)(redux@5.0.1):
dependencies:
'@reduxjs/toolkit': 2.10.1(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)
clsx: 2.1.1
@@ -31503,7 +36805,7 @@ snapshots:
immer: 10.2.0
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
- react-is: 17.0.2
+ react-is: 19.2.6
react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1)
reselect: 5.1.1
tiny-invariant: 1.3.3
@@ -31573,6 +36875,14 @@ snapshots:
get-proto: 1.0.1
which-builtin-type: 1.2.1
+ regenerate-unicode-properties@10.2.2:
+ dependencies:
+ regenerate: 1.4.2
+
+ regenerate@1.4.2: {}
+
+ regenerator-runtime@0.13.11: {}
+
regex-recursion@5.1.1:
dependencies:
regex: 5.1.1
@@ -31603,6 +36913,21 @@ snapshots:
regexpp@3.2.0: {}
+ regexpu-core@6.4.0:
+ dependencies:
+ regenerate: 1.4.2
+ regenerate-unicode-properties: 10.2.2
+ regjsgen: 0.8.0
+ regjsparser: 0.13.1
+ unicode-match-property-ecmascript: 2.0.0
+ unicode-match-property-value-ecmascript: 2.2.1
+
+ regjsgen@0.8.0: {}
+
+ regjsparser@0.13.1:
+ dependencies:
+ jsesc: 3.1.0
+
rehype-parse@9.0.1:
dependencies:
'@types/hast': 3.0.4
@@ -31714,12 +37039,21 @@ snapshots:
resolve-pkg-maps@1.0.0: {}
+ resolve-workspace-root@2.0.1: {}
+
resolve@1.22.10:
dependencies:
is-core-module: 2.16.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
+ resolve@1.22.12:
+ dependencies:
+ es-errors: 1.3.0
+ is-core-module: 2.16.1
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
resolve@2.0.0-next.5:
dependencies:
is-core-module: 2.16.1
@@ -31734,6 +37068,11 @@ snapshots:
dependencies:
lowercase-keys: 3.0.0
+ restore-cursor@2.0.0:
+ dependencies:
+ onetime: 2.0.1
+ signal-exit: 3.0.7
+
restore-cursor@5.1.0:
dependencies:
onetime: 7.0.0
@@ -31749,7 +37088,7 @@ snapshots:
rolldown-plugin-dts@0.16.11(rolldown@1.0.1)(typescript@5.8.3):
dependencies:
- '@babel/generator': 7.28.3
+ '@babel/generator': 7.29.1
'@babel/parser': 7.28.4
'@babel/types': 7.28.4
ast-kit: 2.1.3
@@ -31757,7 +37096,7 @@ snapshots:
debug: 4.4.3(supports-color@8.1.1)
dts-resolver: 2.1.2
get-tsconfig: 4.11.0
- magic-string: 0.30.19
+ magic-string: 0.30.21
rolldown: 1.0.1
optionalDependencies:
typescript: 5.8.3
@@ -31765,6 +37104,24 @@ snapshots:
- oxc-resolver
- supports-color
+ rolldown-plugin-dts@0.16.11(rolldown@1.0.1)(typescript@5.9.3):
+ dependencies:
+ '@babel/generator': 7.29.1
+ '@babel/parser': 7.28.4
+ '@babel/types': 7.28.4
+ ast-kit: 2.1.3
+ birpc: 2.6.1
+ debug: 4.4.3(supports-color@8.1.1)
+ dts-resolver: 2.1.2
+ get-tsconfig: 4.11.0
+ magic-string: 0.30.21
+ rolldown: 1.0.1
+ optionalDependencies:
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - oxc-resolver
+ - supports-color
+
rolldown@1.0.0-beta.42:
dependencies:
'@oxc-project/types': 0.94.0
@@ -31946,6 +37303,8 @@ snapshots:
semver@6.3.1: {}
+ semver@7.6.3: {}
+
semver@7.7.1: {}
semver@7.7.2: {}
@@ -31979,7 +37338,7 @@ snapshots:
escape-html: 1.0.3
etag: 1.8.1
fresh: 2.0.0
- http-errors: 2.0.0
+ http-errors: 2.0.1
mime-types: 3.0.1
ms: 2.1.3
on-finished: 2.4.1
@@ -31990,6 +37349,8 @@ snapshots:
seq-queue@0.0.5: {}
+ serialize-error@2.1.0: {}
+
serialize-javascript@6.0.2:
dependencies:
randombytes: 2.1.0
@@ -32052,11 +37413,15 @@ snapshots:
setprototypeof@1.2.0: {}
+ sf-symbols-typescript@2.2.0: {}
+
+ shallowequal@1.1.0: {}
+
sharp@0.33.5:
dependencies:
color: 4.2.3
detect-libc: 2.0.4
- semver: 7.7.1
+ semver: 7.7.4
optionalDependencies:
'@img/sharp-darwin-arm64': 0.33.5
'@img/sharp-darwin-x64': 0.33.5
@@ -32188,6 +37553,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ simple-plist@1.3.1:
+ dependencies:
+ bplist-creator: 0.1.0
+ bplist-parser: 0.3.1
+ plist: 3.1.1
+
simple-swizzle@0.2.2:
dependencies:
is-arrayish: 0.3.2
@@ -32210,6 +37581,8 @@ snapshots:
astral-regex: 2.0.0
is-fullwidth-code-point: 3.0.0
+ slugify@1.6.9: {}
+
smart-buffer@4.2.0: {}
smob@1.5.0: {}
@@ -32300,8 +37673,8 @@ snapshots:
solid-refresh@0.6.3(solid-js@1.9.6):
dependencies:
- '@babel/generator': 7.27.1
- '@babel/helper-module-imports': 7.27.1
+ '@babel/generator': 7.29.1
+ '@babel/helper-module-imports': 7.28.6
'@babel/types': 7.27.1
solid-js: 1.9.6
transitivePeerDependencies:
@@ -32341,6 +37714,8 @@ snapshots:
buffer-from: 1.1.2
source-map: 0.6.1
+ source-map@0.5.7: {}
+
source-map@0.6.1: {}
source-map@0.7.4: {}
@@ -32367,6 +37742,8 @@ snapshots:
spdx-license-ids@3.0.21: {}
+ split-on-first@1.1.0: {}
+
sprintf-js@1.0.3: {}
sprintf-js@1.1.3: {}
@@ -32424,10 +37801,18 @@ snapshots:
stack-trace@0.0.10: {}
+ stack-utils@2.0.6:
+ dependencies:
+ escape-string-regexp: 2.0.0
+
stackback@0.0.2: {}
stackframe@1.3.4: {}
+ stacktrace-parser@0.1.11:
+ dependencies:
+ type-fest: 0.7.1
+
stacktracey@2.1.8:
dependencies:
as-table: 1.0.55
@@ -32435,6 +37820,8 @@ snapshots:
standard-as-callback@2.1.0: {}
+ statuses@1.5.0: {}
+
statuses@2.0.1: {}
statuses@2.0.2: {}
@@ -32450,16 +37837,16 @@ snapshots:
stoppable@1.1.0: {}
- storybook-solidjs-vite@1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.7.4)))(esbuild@0.25.5)(rollup@4.40.2)(solid-js@1.9.6)(storybook@8.6.12(prettier@3.7.4))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5)):
+ storybook-solidjs-vite@1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.7.4)))(esbuild@0.25.5)(rollup@4.40.2)(solid-js@1.9.6)(storybook@8.6.12(prettier@3.7.4))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5)):
dependencies:
- '@storybook/builder-vite': 10.5.0-alpha.0(esbuild@0.25.5)(rollup@4.40.2)(storybook@8.6.12(prettier@3.7.4))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))
+ '@storybook/builder-vite': 10.5.0-alpha.0(esbuild@0.25.5)(rollup@4.40.2)(storybook@8.6.12(prettier@3.7.4))(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.5))
'@storybook/types': 9.0.0-alpha.1(storybook@8.6.12(prettier@3.7.4))
magic-string: 0.30.17
solid-js: 1.9.6
storybook: 8.6.12(prettier@3.7.4)
storybook-solidjs: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.7.4)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.7.4))
- vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
- vite-plugin-solid: 2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))
+ vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
+ vite-plugin-solid: 2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
transitivePeerDependencies:
- '@storybook/test'
- esbuild
@@ -32490,6 +37877,8 @@ snapshots:
- supports-color
- utf-8-validate
+ stream-buffers@2.2.0: {}
+
streamx@2.22.0:
dependencies:
fast-fifo: 1.3.2
@@ -32497,6 +37886,8 @@ snapshots:
optionalDependencies:
bare-events: 2.5.4
+ strict-uri-encode@2.0.0: {}
+
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
@@ -32578,6 +37969,10 @@ snapshots:
character-entities-html4: 2.1.0
character-entities-legacy: 3.0.0
+ strip-ansi@5.2.0:
+ dependencies:
+ ansi-regex: 4.1.1
+
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
@@ -32626,6 +38021,8 @@ snapshots:
dependencies:
'@tokenizer/token': 0.3.0
+ structured-headers@0.4.1: {}
+
style-to-js@1.1.16:
dependencies:
style-to-object: 1.0.8
@@ -32648,6 +38045,8 @@ snapshots:
client-only: 0.0.1
react: 19.2.4
+ styleq@0.1.3: {}
+
subtitles-parser-vtt@0.1.0: {}
sucrase@3.35.0:
@@ -32676,6 +38075,11 @@ snapshots:
dependencies:
has-flag: 4.0.0
+ supports-hyperlinks@2.3.0:
+ dependencies:
+ has-flag: 4.0.0
+ supports-color: 7.2.0
+
supports-hyperlinks@4.4.0:
dependencies:
has-flag: 5.0.1
@@ -32701,9 +38105,9 @@ snapshots:
dependencies:
tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.8.3))
- tailwind-scrollbar@3.1.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))):
+ tailwind-scrollbar@3.1.0(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))):
dependencies:
- tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))
+ tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))
tailwindcss-animate@1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.8.3))):
dependencies:
@@ -32713,9 +38117,9 @@ snapshots:
dependencies:
tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.8.3))
- tailwindcss-animate@1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))):
+ tailwindcss-animate@1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))):
dependencies:
- tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))
+ tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))
tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.3(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.8.3)):
dependencies:
@@ -32771,7 +38175,34 @@ snapshots:
transitivePeerDependencies:
- ts-node
- tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3)):
+ tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.9.3)):
+ dependencies:
+ '@alloc/quick-lru': 5.2.0
+ arg: 5.0.2
+ chokidar: 3.6.0
+ didyoumean: 1.2.2
+ dlv: 1.1.3
+ fast-glob: 3.3.3
+ glob-parent: 6.0.2
+ is-glob: 4.0.3
+ jiti: 1.21.7
+ lilconfig: 3.1.3
+ micromatch: 4.0.8
+ normalize-path: 3.0.0
+ object-hash: 3.0.0
+ picocolors: 1.1.1
+ postcss: 8.5.3
+ postcss-import: 15.1.0(postcss@8.5.3)
+ postcss-js: 4.0.1(postcss@8.5.3)
+ postcss-load-config: 4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.9.3))
+ postcss-nested: 6.2.0(postcss@8.5.3)
+ postcss-selector-parser: 6.1.2
+ resolve: 1.22.10
+ sucrase: 3.35.0
+ transitivePeerDependencies:
+ - ts-node
+
+ tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3)):
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
@@ -32790,7 +38221,7 @@ snapshots:
postcss: 8.5.3
postcss-import: 15.1.0(postcss@8.5.3)
postcss-js: 4.0.1(postcss@8.5.3)
- postcss-load-config: 4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3))
+ postcss-load-config: 4.0.2(postcss@8.5.3)(ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3))
postcss-nested: 6.2.0(postcss@8.5.3)
postcss-selector-parser: 6.1.2
resolve: 1.22.10
@@ -32840,6 +38271,11 @@ snapshots:
dependencies:
'@tauri-apps/api': 1.6.0
+ terminal-link@2.1.1:
+ dependencies:
+ ansi-escapes: 4.3.2
+ supports-hyperlinks: 2.3.0
+
terminal-link@5.0.0:
dependencies:
ansi-escapes: 7.2.0
@@ -32875,7 +38311,12 @@ snapshots:
acorn: 8.16.0
commander: 2.20.3
source-map-support: 0.5.21
- optional: true
+
+ test-exclude@6.0.0:
+ dependencies:
+ '@istanbuljs/schema': 0.1.3
+ glob: 7.2.3
+ minimatch: 3.1.2
test-exclude@7.0.1:
dependencies:
@@ -32899,6 +38340,8 @@ snapshots:
dependencies:
any-promise: 1.3.0
+ throat@5.0.0: {}
+
through@2.3.8: {}
thunky@1.1.0: {}
@@ -32953,6 +38396,8 @@ snapshots:
tmp@0.2.5: {}
+ tmpl@1.0.5: {}
+
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -32969,6 +38414,8 @@ snapshots:
toml@3.0.0: {}
+ toqr@0.1.1: {}
+
totalist@3.0.1: {}
tough-cookie@5.1.2:
@@ -33047,7 +38494,29 @@ snapshots:
'@swc/wasm': 1.15.5
optional: true
- ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.8.3):
+ ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@20.17.43)(typescript@5.9.3):
+ dependencies:
+ '@cspotcode/source-map-support': 0.8.1
+ '@tsconfig/node10': 1.0.11
+ '@tsconfig/node12': 1.0.11
+ '@tsconfig/node14': 1.0.3
+ '@tsconfig/node16': 1.0.4
+ '@types/node': 20.17.43
+ acorn: 8.16.0
+ acorn-walk: 8.3.4
+ arg: 4.1.3
+ create-require: 1.1.1
+ diff: 4.0.2
+ make-error: 1.3.6
+ typescript: 5.9.3
+ v8-compile-cache-lib: 3.0.1
+ yn: 3.1.1
+ optionalDependencies:
+ '@swc/core': 1.15.5(@swc/helpers@0.5.17)
+ '@swc/wasm': 1.15.5
+ optional: true
+
+ ts-node@10.9.2(@swc/core@1.15.5(@swc/helpers@0.5.17))(@swc/wasm@1.15.5)(@types/node@22.15.17)(typescript@5.9.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
@@ -33061,7 +38530,7 @@ snapshots:
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
- typescript: 5.8.3
+ typescript: 5.9.3
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
optionalDependencies:
@@ -33073,6 +38542,10 @@ snapshots:
optionalDependencies:
typescript: 5.8.3
+ tsconfck@3.1.5(typescript@5.9.3):
+ optionalDependencies:
+ typescript: 5.9.3
+
tsconfig-paths@3.15.0:
dependencies:
'@types/json5': 0.0.29
@@ -33111,6 +38584,31 @@ snapshots:
- supports-color
- vue-tsc
+ tsdown@0.15.6(typescript@5.9.3):
+ dependencies:
+ ansis: 4.2.0
+ cac: 6.7.14
+ chokidar: 4.0.3
+ debug: 4.4.3(supports-color@8.1.1)
+ diff: 8.0.2
+ empathic: 2.0.0
+ hookable: 5.5.3
+ rolldown: 1.0.1
+ rolldown-plugin-dts: 0.16.11(rolldown@1.0.1)(typescript@5.9.3)
+ semver: 7.7.2
+ tinyexec: 1.0.1
+ tinyglobby: 0.2.15
+ tree-kill: 1.2.2
+ unconfig: 7.3.3
+ optionalDependencies:
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - '@ts-macro/tsc'
+ - '@typescript/native-preview'
+ - oxc-resolver
+ - supports-color
+ - vue-tsc
+
tslib@1.14.1: {}
tslib@2.6.2: {}
@@ -33146,10 +38644,39 @@ snapshots:
- tsx
- yaml
- tsutils@3.21.0(typescript@5.8.3):
+ tsup@8.5.0(@swc/core@1.15.5(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.3)(typescript@5.9.3)(yaml@2.8.1):
+ dependencies:
+ bundle-require: 5.1.0(esbuild@0.25.12)
+ cac: 6.7.14
+ chokidar: 4.0.3
+ consola: 3.4.2
+ debug: 4.4.3(supports-color@8.1.1)
+ esbuild: 0.25.12
+ fix-dts-default-cjs-exports: 1.0.1
+ joycon: 3.1.1
+ picocolors: 1.1.1
+ postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.3)(yaml@2.8.1)
+ resolve-from: 5.0.0
+ rollup: 4.40.2
+ source-map: 0.8.0-beta.0
+ sucrase: 3.35.0
+ tinyexec: 0.3.2
+ tinyglobby: 0.2.15
+ tree-kill: 1.2.2
+ optionalDependencies:
+ '@swc/core': 1.15.5(@swc/helpers@0.5.17)
+ postcss: 8.5.3
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - jiti
+ - supports-color
+ - tsx
+ - yaml
+
+ tsutils@3.21.0(typescript@5.9.3):
dependencies:
tslib: 1.14.1
- typescript: 5.8.3
+ typescript: 5.9.3
tsyringe@4.10.0:
dependencies:
@@ -33198,10 +38725,14 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
+ type-detect@4.0.8: {}
+
type-fest@0.20.2: {}
type-fest@0.21.3: {}
+ type-fest@0.7.1: {}
+
type-fest@4.41.0: {}
type-is@1.6.18:
@@ -33263,6 +38794,8 @@ snapshots:
typescript@5.8.3: {}
+ typescript@5.9.3: {}
+
ua-parser-js@1.0.41: {}
ufo@1.6.1: {}
@@ -33304,7 +38837,7 @@ snapshots:
dependencies:
acorn: 8.14.1
estree-walker: 3.0.3
- magic-string: 0.30.17
+ magic-string: 0.30.21
unplugin: 2.3.2
unctx@2.5.0:
@@ -33366,6 +38899,17 @@ snapshots:
pathe: 2.0.3
ufo: 1.6.1
+ unicode-canonical-property-names-ecmascript@2.0.1: {}
+
+ unicode-match-property-ecmascript@2.0.0:
+ dependencies:
+ unicode-canonical-property-names-ecmascript: 2.0.1
+ unicode-property-aliases-ecmascript: 2.2.0
+
+ unicode-match-property-value-ecmascript@2.2.1: {}
+
+ unicode-property-aliases-ecmascript@2.2.0: {}
+
unicorn-magic@0.1.0: {}
unicorn-magic@0.3.0: {}
@@ -33388,10 +38932,10 @@ snapshots:
estree-walker: 3.0.3
fast-glob: 3.3.3
local-pkg: 1.1.1
- magic-string: 0.30.17
+ magic-string: 0.30.21
mlly: 1.7.4
pathe: 2.0.3
- picomatch: 4.0.2
+ picomatch: 4.0.3
pkg-types: 1.3.1
scule: 1.3.0
strip-literal: 2.1.1
@@ -33405,7 +38949,7 @@ snapshots:
escape-string-regexp: 5.0.0
estree-walker: 3.0.3
local-pkg: 1.1.1
- magic-string: 0.30.19
+ magic-string: 0.30.21
mlly: 1.8.0
pathe: 2.0.3
picomatch: 4.0.3
@@ -33505,11 +39049,11 @@ snapshots:
transitivePeerDependencies:
- rollup
- unplugin-fonts@1.3.1(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)):
+ unplugin-fonts@1.3.1(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)):
dependencies:
fast-glob: 3.3.3
unplugin: 2.0.0-beta.1
- vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
unplugin-icons@0.19.3:
dependencies:
@@ -33612,7 +39156,7 @@ snapshots:
unwasm@0.3.9:
dependencies:
knitwork: 1.2.0
- magic-string: 0.30.19
+ magic-string: 0.30.21
mlly: 1.8.0
pathe: 1.1.2
pkg-types: 1.3.1
@@ -33640,6 +39184,12 @@ snapshots:
escalade: 3.2.0
picocolors: 1.1.1
+ update-browserslist-db@1.2.3(browserslist@4.28.2):
+ dependencies:
+ browserslist: 4.28.2
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
uqr@0.1.2: {}
uri-js@4.4.1:
@@ -33657,6 +39207,13 @@ snapshots:
urlpattern-polyfill@8.0.2: {}
+ use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4):
dependencies:
react: 19.2.4
@@ -33669,12 +39226,24 @@ snapshots:
dequal: 2.0.3
react: 19.2.4
+ use-latest-callback@0.2.6(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+
use-resize-observer@9.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@juggle/resize-observer': 3.4.0
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
+ use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.0):
+ dependencies:
+ detect-node-es: 1.1.0
+ react: 19.2.0
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4):
dependencies:
detect-node-es: 1.1.0
@@ -33683,6 +39252,10 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
+ use-sync-external-store@1.5.0(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+
use-sync-external-store@1.5.0(react@19.2.4):
dependencies:
react: 19.2.4
@@ -33703,6 +39276,8 @@ snapshots:
uuid@11.1.0: {}
+ uuid@7.0.3: {}
+
uuid@8.0.0: {}
uuid@8.3.2: {}
@@ -33718,6 +39293,11 @@ snapshots:
optionalDependencies:
typescript: 5.8.3
+ valibot@1.0.0-rc.1(typescript@5.9.3):
+ optionalDependencies:
+ typescript: 5.9.3
+ optional: true
+
validate-html-nesting@1.2.2: {}
validate-npm-package-license@3.0.4:
@@ -33729,6 +39309,15 @@ snapshots:
vary@1.1.2: {}
+ vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ dependencies:
+ '@radix-ui/react-dialog': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ - '@types/react-dom'
+
vfile-location@5.0.3:
dependencies:
'@types/unist': 3.0.3
@@ -33766,7 +39355,7 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
- vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1):
+ vinxi@0.5.6(@planetscale/database@1.19.0)(@types/node@22.15.17)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(encoding@0.1.13)(ioredis@5.6.1)(jiti@2.6.1)(lightningcss@1.32.0)(mysql2@3.15.2)(rolldown@1.0.1)(terser@5.44.0)(xml2js@0.6.2)(yaml@2.8.1):
dependencies:
'@babel/core': 7.27.1
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.27.1)
@@ -33800,7 +39389,7 @@ snapshots:
unctx: 2.4.1
unenv: 1.10.0
unstorage: 1.16.0(@planetscale/database@1.19.0)(db0@0.3.2(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20250507.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(bun-types@1.3.14)(mysql2@3.15.2))(mysql2@3.15.2))(ioredis@5.6.1)
- vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
zod: 3.25.76
transitivePeerDependencies:
- '@azure/app-configuration'
@@ -33844,15 +39433,34 @@ snapshots:
- xml2js
- yaml
- vite-node@2.1.9(@types/node@22.15.17)(terser@5.44.0):
+ vite-node@2.1.9(@types/node@22.15.17)(lightningcss@1.32.0)(terser@5.44.0):
dependencies:
cac: 6.7.14
- debug: 4.4.1
+ debug: 4.4.3(supports-color@8.1.1)
es-module-lexer: 1.7.0
pathe: 1.1.2
- vite: 5.4.19(@types/node@22.15.17)(terser@5.44.0)
+ vite: 5.4.19(@types/node@22.15.17)(lightningcss@1.32.0)(terser@5.44.0)
+ transitivePeerDependencies:
+ - '@types/node'
+ - less
+ - lightningcss
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+
+ vite-node@3.2.4(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1):
+ dependencies:
+ cac: 6.7.14
+ debug: 4.4.3(supports-color@8.1.1)
+ es-module-lexer: 1.7.0
+ pathe: 2.0.3
+ vite: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- '@types/node'
+ - jiti
- less
- lightningcss
- sass
@@ -33861,14 +39469,16 @@ snapshots:
- sugarss
- supports-color
- terser
+ - tsx
+ - yaml
- vite-node@3.2.4(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1):
+ vite-node@3.2.4(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1):
dependencies:
cac: 6.7.14
debug: 4.4.3(supports-color@8.1.1)
es-module-lexer: 1.7.0
pathe: 2.0.3
- vite: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -33883,7 +39493,7 @@ snapshots:
- tsx
- yaml
- vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)):
+ vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)):
dependencies:
'@babel/core': 7.27.1
'@types/babel__core': 7.20.5
@@ -33891,51 +39501,62 @@ snapshots:
merge-anything: 5.1.7
solid-js: 1.9.6
solid-refresh: 0.6.3(solid-js@1.9.6)
- vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
- vitefu: 1.0.6(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))
+ vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
+ vitefu: 1.0.6(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
optionalDependencies:
'@testing-library/jest-dom': 6.5.0
transitivePeerDependencies:
- supports-color
- vite-plugin-top-level-await@1.6.0(@swc/helpers@0.5.17)(rollup@4.40.2)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)):
+ vite-plugin-top-level-await@1.6.0(@swc/helpers@0.5.17)(rollup@4.40.2)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)):
dependencies:
'@rollup/plugin-virtual': 3.0.2(rollup@4.40.2)
'@swc/core': 1.15.5(@swc/helpers@0.5.17)
'@swc/wasm': 1.15.5
uuid: 10.0.0
- vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- '@swc/helpers'
- rollup
- vite-plugin-wasm@3.5.0(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)):
+ vite-plugin-wasm@3.5.0(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)):
dependencies:
- vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
- vite-tsconfig-paths@4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)):
+ vite-tsconfig-paths@4.3.2(typescript@5.8.3)(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)):
dependencies:
debug: 4.4.0
globrex: 0.1.2
tsconfck: 3.1.5(typescript@5.8.3)
optionalDependencies:
- vite: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ vite: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ vite-tsconfig-paths@4.3.2(typescript@5.9.3)(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)):
+ dependencies:
+ debug: 4.4.0
+ globrex: 0.1.2
+ tsconfck: 3.1.5(typescript@5.9.3)
+ optionalDependencies:
+ vite: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- supports-color
- typescript
- vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)):
+ vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)):
dependencies:
debug: 4.4.0
globrex: 0.1.2
tsconfck: 3.1.5(typescript@5.8.3)
optionalDependencies:
- vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- supports-color
- typescript
- vite@5.4.19(@types/node@22.15.17)(terser@5.44.0):
+ vite@5.4.19(@types/node@22.15.17)(lightningcss@1.32.0)(terser@5.44.0):
dependencies:
esbuild: 0.21.5
postcss: 8.5.3
@@ -33943,9 +39564,10 @@ snapshots:
optionalDependencies:
'@types/node': 22.15.17
fsevents: 2.3.3
+ lightningcss: 1.32.0
terser: 5.44.0
- vite@6.1.4(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1):
+ vite@6.1.4(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1):
dependencies:
esbuild: 0.24.2
postcss: 8.5.3
@@ -33954,10 +39576,11 @@ snapshots:
'@types/node': 22.15.17
fsevents: 2.3.3
jiti: 2.6.1
+ lightningcss: 1.32.0
terser: 5.44.0
yaml: 2.8.1
- vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1):
+ vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1):
dependencies:
esbuild: 0.25.4
fdir: 6.4.4(picomatch@4.0.2)
@@ -33969,10 +39592,11 @@ snapshots:
'@types/node': 20.17.43
fsevents: 2.3.3
jiti: 2.6.1
+ lightningcss: 1.32.0
terser: 5.44.0
yaml: 2.8.1
- vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1):
+ vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1):
dependencies:
esbuild: 0.25.4
fdir: 6.4.4(picomatch@4.0.2)
@@ -33984,17 +39608,18 @@ snapshots:
'@types/node': 22.15.17
fsevents: 2.3.3
jiti: 2.6.1
+ lightningcss: 1.32.0
terser: 5.44.0
yaml: 2.8.1
- vitefu@1.0.6(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)):
+ vitefu@1.0.6(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)):
optionalDependencies:
- vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
- vitest@2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(terser@5.44.0):
+ vitest@2.1.9(@types/node@22.15.17)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.44.0):
dependencies:
'@vitest/expect': 2.1.9
- '@vitest/mocker': 2.1.9(vite@5.4.19(@types/node@22.15.17)(terser@5.44.0))
+ '@vitest/mocker': 2.1.9(vite@5.4.19(@types/node@22.15.17)(lightningcss@1.32.0)(terser@5.44.0))
'@vitest/pretty-format': 2.1.9
'@vitest/runner': 2.1.9
'@vitest/snapshot': 2.1.9
@@ -34010,8 +39635,8 @@ snapshots:
tinyexec: 0.3.2
tinypool: 1.0.2
tinyrainbow: 1.2.0
- vite: 5.4.19(@types/node@22.15.17)(terser@5.44.0)
- vite-node: 2.1.9(@types/node@22.15.17)(terser@5.44.0)
+ vite: 5.4.19(@types/node@22.15.17)(lightningcss@1.32.0)(terser@5.44.0)
+ vite-node: 2.1.9(@types/node@22.15.17)(lightningcss@1.32.0)(terser@5.44.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.15.17
@@ -34027,11 +39652,11 @@ snapshots:
- supports-color
- terser
- vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.17.43)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.44.0)(yaml@2.8.1):
+ vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.17.43)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.4
- '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))
+ '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
@@ -34040,7 +39665,7 @@ snapshots:
chai: 5.2.0
debug: 4.4.3(supports-color@8.1.1)
expect-type: 1.2.1
- magic-string: 0.30.19
+ magic-string: 0.30.21
pathe: 2.0.3
picomatch: 4.0.3
std-env: 3.9.0
@@ -34049,8 +39674,8 @@ snapshots:
tinyglobby: 0.2.15
tinypool: 1.1.1
tinyrainbow: 2.0.0
- vite: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
- vite-node: 3.2.4(@types/node@20.17.43)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1)
+ vite: 6.3.5(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
+ vite-node: 3.2.4(@types/node@20.17.43)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/debug': 4.1.12
@@ -34071,12 +39696,64 @@ snapshots:
- tsx
- yaml
+ vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.15.17)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1):
+ dependencies:
+ '@types/chai': 5.2.3
+ '@vitest/expect': 3.2.4
+ '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1))
+ '@vitest/pretty-format': 3.2.4
+ '@vitest/runner': 3.2.4
+ '@vitest/snapshot': 3.2.4
+ '@vitest/spy': 3.2.4
+ '@vitest/utils': 3.2.4
+ chai: 5.2.0
+ debug: 4.4.3(supports-color@8.1.1)
+ expect-type: 1.2.1
+ magic-string: 0.30.21
+ pathe: 2.0.3
+ picomatch: 4.0.3
+ std-env: 3.9.0
+ tinybench: 2.9.0
+ tinyexec: 0.3.2
+ tinyglobby: 0.2.15
+ tinypool: 1.1.1
+ tinyrainbow: 2.0.0
+ vite: 6.3.5(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
+ vite-node: 3.2.4(@types/node@22.15.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.44.0)(yaml@2.8.1)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/debug': 4.1.12
+ '@types/node': 22.15.17
+ '@vitest/ui': 3.2.4(vitest@3.2.4)
+ jsdom: 26.1.0
+ transitivePeerDependencies:
+ - jiti
+ - less
+ - lightningcss
+ - msw
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ - tsx
+ - yaml
+
+ vlq@1.0.1: {}
+
w3c-xmlserializer@5.0.0:
dependencies:
xml-name-validator: 5.0.0
walk-up-path@3.0.1: {}
+ walker@1.0.8:
+ dependencies:
+ makeerror: 1.0.12
+
+ warn-once@0.1.1: {}
+
watchpack@2.5.1:
dependencies:
glob-to-regexp: 0.4.1
@@ -34085,7 +39762,6 @@ snapshots:
wcwidth@1.0.1:
dependencies:
defaults: 1.0.4
- optional: true
web-namespaces@2.0.1: {}
@@ -34126,7 +39802,7 @@ snapshots:
'@webassemblyjs/wasm-parser': 1.14.1
acorn: 8.16.0
acorn-import-phases: 1.0.4(acorn@8.16.0)
- browserslist: 4.26.3
+ browserslist: 4.28.2
chrome-trace-event: 1.0.4
enhanced-resolve: 5.19.0
es-module-lexer: 1.7.0
@@ -34222,8 +39898,12 @@ snapshots:
dependencies:
iconv-lite: 0.6.3
+ whatwg-fetch@3.6.20: {}
+
whatwg-mimetype@4.0.0: {}
+ whatwg-url-minimum@0.1.2: {}
+
whatwg-url@14.2.0:
dependencies:
tr46: 5.1.1
@@ -34346,14 +40026,14 @@ snapshots:
'@cloudflare/workerd-linux-arm64': 1.20250408.0
'@cloudflare/workerd-windows-64': 1.20250408.0
- workflow@4.2.0-beta.73(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@opentelemetry/api@1.9.0)(@swc/cli@0.8.0(@swc/core@1.15.3(@swc/helpers@0.5.17))(chokidar@5.0.0))(@swc/core@1.15.3(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(magicast@0.3.5)(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.8.3):
+ workflow@4.2.0-beta.73(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@opentelemetry/api@1.9.0)(@swc/cli@0.8.0(@swc/core@1.15.3(@swc/helpers@0.5.17))(chokidar@5.0.0))(@swc/core@1.15.3(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(magicast@0.3.5)(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.8.3):
dependencies:
'@workflow/astro': 4.0.0-beta.47(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)
'@workflow/cli': 4.2.0-beta.73(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)
'@workflow/core': 4.2.0-beta.73(@opentelemetry/api@1.9.0)
'@workflow/errors': 4.1.0-beta.19
'@workflow/nest': 0.0.0-beta.22(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17(@nestjs/common@11.1.17(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@opentelemetry/api@1.9.0)(@swc/cli@0.8.0(@swc/core@1.15.3(@swc/helpers@0.5.17))(chokidar@5.0.0))(@swc/core@1.15.3(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)
- '@workflow/next': 4.0.1-beta.69(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
+ '@workflow/next': 4.0.1-beta.69(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)(next@16.2.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
'@workflow/nitro': 4.0.1-beta.68(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)
'@workflow/nuxt': 4.0.1-beta.57(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)(magicast@0.3.5)
'@workflow/rollup': 4.0.0-beta.30(@opentelemetry/api@1.9.0)(@swc/helpers@0.5.17)
@@ -34434,6 +40114,11 @@ snapshots:
wrappy@1.0.2: {}
+ write-file-atomic@4.0.2:
+ dependencies:
+ imurmurhash: 0.1.4
+ signal-exit: 3.0.7
+
write-file-atomic@5.0.1:
dependencies:
imurmurhash: 0.1.4
@@ -34444,6 +40129,8 @@ snapshots:
imurmurhash: 0.1.4
signal-exit: 4.1.0
+ ws@7.5.10: {}
+
ws@8.17.1: {}
ws@8.18.0: {}
@@ -34456,6 +40143,11 @@ snapshots:
dependencies:
is-wsl: 3.1.0
+ xcode@3.0.1:
+ dependencies:
+ simple-plist: 1.3.1
+ uuid: 7.0.3
+
xdg-app-paths@5.1.0:
dependencies:
xdg-portable: 7.3.0
@@ -34466,6 +40158,11 @@ snapshots:
xml-name-validator@5.0.0: {}
+ xml2js@0.6.0:
+ dependencies:
+ sax: 1.2.1
+ xmlbuilder: 11.0.1
+
xml2js@0.6.2:
dependencies:
sax: 1.2.1
@@ -34473,6 +40170,8 @@ snapshots:
xmlbuilder@11.0.1: {}
+ xmlbuilder@15.1.1: {}
+
xmlchars@2.2.0: {}
y18n@5.0.8: {}