feat(mobile): in-app document viewer (#667)

* Keep the phone logged in

* Added document sheet

* Make button outline

* Fix colors

* Design accroding to base design

* Design accroding to base design

* Added download and share

* Added view document screen

* Added view document screen

* Screen is launching

* fix toolbar issue

* Fix the button

* fix all the conflicts

* revert unncessary bloat

* remove packages

* pnpnm i

* Fixed some issues

* Added error state

* added header in loading and error state

* Removed duplication  in render

* added correct name of app

* Update apps/mobile/app.json

Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>

---------

Co-authored-by: jibraniqbal666 <jibran.iqbal@protonmail.com>
Co-authored-by: Corentin Thomasset <corentin.thomasset74@gmail.com>
This commit is contained in:
Jibran Iqbal
2025-12-12 17:27:36 +01:00
committed by GitHub
parent cf91515cfe
commit 9d9be949b0
9 changed files with 367 additions and 34 deletions

View File

@@ -1,7 +1,7 @@
{
"expo": {
"name": "mobile",
"slug": "mobile",
"name": "Papra",
"slug": "papra",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./src/assets/images/icon.png",
@@ -19,7 +19,8 @@
"monochromeImage": "./src/assets/images/android-icon-monochrome.png"
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false
"predictiveBackGestureEnabled": false,
"package": "app.papra.android"
},
"web": {
"output": "static",

View File

@@ -11,6 +11,14 @@ export default function RootLayout() {
<Stack.Screen name="auth/login" options={{ headerShown: false }} />
<Stack.Screen name="auth/signup" options={{ headerShown: false }} />
<Stack.Screen name="(with-organizations)" options={{ headerShown: false }} />
<Stack.Screen
name="document/view"
options={{
headerShown: false,
presentation: 'modal',
animation: 'slide_from_bottom',
}}
/>
</Stack>
<StatusBar style="auto" />
</ApiProvider>

View File

@@ -0,0 +1,3 @@
import DocumentViewScreen from '@/modules/documents-actions/screens/document-view.screen';
export default DocumentViewScreen;

View File

@@ -7,8 +7,8 @@
"scripts": {
"dev": "pnpm start",
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
@@ -48,6 +48,7 @@
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-gesture-handler": "~2.28.0",
"react-native-pdf": "^7.0.3",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",

View File

@@ -2,6 +2,7 @@ import type { CoerceDates } from '@/modules/api/api.models';
import type { Document } from '@/modules/documents/documents.types';
import type { ThemeColors } from '@/modules/ui/theme.constants';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { router } from 'expo-router';
import * as Sharing from 'expo-sharing';
import {
Modal,
@@ -21,14 +22,12 @@ type DocumentActionSheetProps = {
visible: boolean;
document: CoerceDates<Document> | undefined;
onClose: () => void;
onView: () => void;
};
export function DocumentActionSheet({
visible,
document,
onClose,
onView,
}: DocumentActionSheetProps) {
const themeColors = useThemeColor();
const styles = createStyles({ themeColors });
@@ -64,6 +63,17 @@ export function DocumentActionSheet({
});
};
const handleView = async () => {
onClose();
router.push({
pathname: '/(app)/document/view',
params: {
documentId: document.id,
organizationId: document.organizationId,
},
});
};
const handleDownloadAndShare = async () => {
const baseUrl = await configLocalStorage.getApiServerBaseUrl();
@@ -166,9 +176,9 @@ export function DocumentActionSheet({
{isViewable && (
<TouchableOpacity
style={styles.actionButton}
onPress={() => {
onPress={async () => {
onClose();
onView();
await handleView();
}}
activeOpacity={0.7}
>

View File

@@ -0,0 +1,257 @@
import type { CoerceDates } from '@/modules/api/api.models';
import type { Document } from '@/modules/documents/documents.types';
import type { ThemeColors } from '@/modules/ui/theme.constants';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useQuery } from '@tanstack/react-query';
import { useLocalSearchParams, useRouter } from 'expo-router';
import React from 'react';
import {
ActivityIndicator,
Image,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import Pdf from 'react-native-pdf';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useApiClient, useAuthClient } from '@/modules/api/providers/api.provider';
import { configLocalStorage } from '@/modules/config/config.local-storage';
import { fetchDocument, fetchDocumentFile } from '@/modules/documents/documents.services';
import { useAlert } from '@/modules/ui/providers/alert-provider';
import { useThemeColor } from '@/modules/ui/providers/use-theme-color';
type DocumentFile = {
uri: string;
doc: CoerceDates<Document>;
};
export default function DocumentViewScreen() {
const router = useRouter();
const params = useLocalSearchParams<{ documentId: string; organizationId: string }>();
const themeColors = useThemeColor();
const styles = createStyles({ themeColors });
const { showAlert } = useAlert();
const apiClient = useApiClient();
const authClient = useAuthClient();
const { documentId, organizationId } = params;
const documentQuery = useQuery({
queryKey: ['organizations', organizationId, 'documents', documentId],
queryFn: async () => {
if (organizationId == null || documentId == null) {
throw new Error('Organization ID and Document ID are required');
}
return fetchDocument({ organizationId, documentId, apiClient });
},
enabled: organizationId != null && documentId != null,
});
const documentFileQuery = useQuery({
queryKey: ['organizations', organizationId, 'documents', documentId, 'file'],
queryFn: async () => {
if (documentQuery.data == null) {
throw new Error('Document not loaded');
}
const baseUrl = await configLocalStorage.getApiServerBaseUrl();
if (baseUrl == null) {
throw new Error('Base URL not found');
}
const fileUri = await fetchDocumentFile({
document: documentQuery.data.document,
organizationId,
baseUrl,
authClient,
});
return {
uri: fileUri,
doc: documentQuery.data.document,
} as DocumentFile;
},
enabled: documentQuery.isSuccess && documentQuery.data != null,
});
const renderHeader = (documentName: string) => {
return (
<View style={styles.header}>
<TouchableOpacity
style={styles.backButton}
onPress={() => router.back()}
>
<MaterialCommunityIcons
name="close"
size={24}
color={themeColors.foreground}
/>
</TouchableOpacity>
<Text style={styles.headerTitle} numberOfLines={1}>
{documentName}
</Text>
<View style={styles.headerSpacer} />
</View>
);
};
const renderDocumentFile = (file: DocumentFile) => {
if (file.doc.mimeType.startsWith('image/')) {
return (
<Image
source={{ uri: file.uri }}
style={styles.pdfViewer}
/>
);
}
if (file.doc.mimeType.startsWith('application/pdf')) {
return (
<Pdf
source={{ uri: file.uri, cache: true }}
style={styles.pdfViewer}
onError={(error) => {
console.error('PDF error:', error);
showAlert({
title: 'Error',
message: 'Failed to load PDF',
});
}}
enablePaging={true}
horizontal={false}
enableAnnotationRendering={true}
fitPolicy={0}
spacing={10}
/>
);
}
return <View style={styles.pdfViewer} />;
};
const isLoading = documentQuery.isLoading || documentFileQuery.isLoading;
const error = documentQuery.error ?? documentFileQuery.error;
const documentFile = documentFileQuery.data;
const documentName = documentFile?.doc.name ?? 'Document';
return (
<SafeAreaView style={styles.container}>
{renderHeader(documentName)}
{isLoading
? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={themeColors.primary} />
<Text style={styles.loadingText}>Loading document...</Text>
</View>
)
: error != null
? (
<View style={styles.errorContainer}>
<MaterialCommunityIcons
name="file-pdf-box"
size={64}
color={themeColors.mutedForeground}
/>
<Text style={styles.errorText}>Failed to load document</Text>
<TouchableOpacity
style={styles.errorButton}
onPress={() => {
void documentQuery.refetch();
}}
>
<Text style={styles.errorButtonText}>Retry</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.errorButton}
onPress={() => router.back()}
>
<Text style={styles.errorButtonText}>Go Back</Text>
</TouchableOpacity>
</View>
)
: documentFile != null
? (
<View style={styles.pdfContainer}>
{renderDocumentFile(documentFile)}
</View>
)
: null}
</SafeAreaView>
);
}
function createStyles({ themeColors }: { themeColors: ThemeColors }) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: themeColors.background,
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 24,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: themeColors.border,
},
backButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: themeColors.secondaryBackground,
justifyContent: 'center',
alignItems: 'flex-start',
},
headerTitle: {
flex: 1,
fontSize: 18,
fontWeight: 'bold',
color: themeColors.foreground,
marginHorizontal: 16,
},
headerSpacer: {
width: 40,
},
pdfContainer: {
flex: 1,
backgroundColor: themeColors.background,
},
pdfViewer: {
flex: 1,
width: '100%',
height: '100%',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: 16,
fontSize: 16,
color: themeColors.mutedForeground,
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 32,
},
errorText: {
fontSize: 18,
color: themeColors.foreground,
marginTop: 16,
marginBottom: 24,
},
errorButton: {
paddingHorizontal: 24,
paddingVertical: 16,
backgroundColor: themeColors.secondaryBackground,
borderRadius: 12,
marginTop: 16,
},
errorButtonText: {
fontSize: 16,
fontWeight: '600',
color: themeColors.primary,
},
});
}

View File

@@ -76,6 +76,24 @@ export async function fetchOrganizationDocuments({
}
}
export async function fetchDocument({
organizationId,
documentId,
apiClient,
}: {
organizationId: string;
documentId: string;
apiClient: ApiClient;
}) {
const { document } = await apiClient<{ document: Document }>({
method: 'GET',
path: `/api/organizations/${organizationId}/documents/${documentId}`,
});
return {
document: coerceDates(document),
};
}
export async function fetchDocumentFile({
document,
organizationId,

View File

@@ -85,7 +85,6 @@ export function DocumentsListScreen() {
visible={true}
document={onDocumentActionSheet}
onClose={() => setOnDocumentActionSheet(undefined)}
onView={() => {}}
/>
)}
<View style={styles.header}>

84
pnpm-lock.yaml generated
View File

@@ -225,6 +225,9 @@ importers:
react-native-gesture-handler:
specifier: ~2.28.0
version: 2.28.0(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-pdf:
specifier: ^7.0.3
version: 7.0.3(react-native-blob-util@0.23.2(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-reanimated:
specifier: ~4.1.1
version: 4.1.5(@babel/core@7.28.4)(react-native-worklets@0.5.1(@babel/core@7.28.4)(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -4046,6 +4049,9 @@ packages:
resolution: {integrity: sha512-fB7M1CMOCIUudTRuj7kzxIBTVw2KXnsgbQ6+4cbqSxo8NmRRhA0Ul4ZUzZj3rFd3VznTL4Brmocv1oiN0bWZ8w==}
engines: {node: '>= 20.19.4'}
'@react-native/normalize-color@2.1.0':
resolution: {integrity: sha512-Z1jQI2NpdFJCVgpY+8Dq/Bt3d+YUi1928Q+/CZm/oh66fzM0RUl54vvuXlPJKybH4pdCZey1eDTPaLHkMPNgWA==}
'@react-native/normalize-colors@0.74.89':
resolution: {integrity: sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==}
@@ -5874,6 +5880,9 @@ packages:
bare-events:
optional: true
base-64@0.1.0:
resolution: {integrity: sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==}
base-64@1.0.0:
resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==}
@@ -6403,6 +6412,9 @@ packages:
crypt@0.0.2:
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
crypto-random-string@2.0.0:
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
engines: {node: '>=8'}
@@ -6560,6 +6572,9 @@ packages:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
deprecated-react-native-prop-types@2.3.0:
resolution: {integrity: sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==}
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
@@ -7824,9 +7839,6 @@ packages:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'}
get-tsconfig@4.10.0:
resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==}
get-tsconfig@4.10.1:
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
@@ -10091,6 +10103,12 @@ packages:
react-is@19.2.0:
resolution: {integrity: sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==}
react-native-blob-util@0.23.2:
resolution: {integrity: sha512-ZsUUFQYyZ7BI57c31XdPCkPlteoH7+PvcVy2w6wh1OPSUWGtKL79pj7fa6MepMX0v87fn0V9Heq0n6OjEpLdCw==}
peerDependencies:
react: '*'
react-native: '*'
react-native-gesture-handler@2.28.0:
resolution: {integrity: sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==}
peerDependencies:
@@ -10103,6 +10121,13 @@ packages:
react: '*'
react-native: '*'
react-native-pdf@7.0.3:
resolution: {integrity: sha512-zDtF6CGXPAfGptQZqX7LQK3CVQrIGsD+rYuBnMK0sVmd8mrq7ciwmWXINT+d92emMtZ7+PLnx1IQZIdsh0fphA==}
peerDependencies:
react: '*'
react-native: '*'
react-native-blob-util: '>=0.13.7'
react-native-reanimated@4.1.5:
resolution: {integrity: sha512-UA6VUbxwhRjEw2gSNrvhkusUq3upfD3Cv+AnB07V+kC8kpvwRVI+ivwY95ePbWNFkFpP+Y2Sdw1WHpHWEV+P2Q==}
peerDependencies:
@@ -11264,11 +11289,6 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsx@4.20.3:
resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==}
engines: {node: '>=18.0.0'}
hasBin: true
tsx@4.20.6:
resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==}
engines: {node: '>=18.0.0'}
@@ -14046,7 +14066,7 @@ snapshots:
'@esbuild-kit/esm-loader@2.6.5':
dependencies:
'@esbuild-kit/core-utils': 3.3.2
get-tsconfig: 4.10.0
get-tsconfig: 4.10.1
'@esbuild/aix-ppc64@0.19.12':
optional: true
@@ -16421,6 +16441,8 @@ snapshots:
'@react-native/js-polyfills@0.81.5': {}
'@react-native/normalize-color@2.1.0': {}
'@react-native/normalize-colors@0.74.89': {}
'@react-native/normalize-colors@0.76.2': {}
@@ -18705,6 +18727,8 @@ snapshots:
bare-events: 2.5.4
optional: true
base-64@0.1.0: {}
base-64@1.0.0: {}
base64-js@1.5.1: {}
@@ -19274,6 +19298,8 @@ snapshots:
crypt@0.0.2: {}
crypto-js@4.2.0: {}
crypto-random-string@2.0.0: {}
css-in-js-utils@3.1.0:
@@ -19406,6 +19432,12 @@ snapshots:
depd@2.0.0: {}
deprecated-react-native-prop-types@2.3.0:
dependencies:
'@react-native/normalize-color': 2.1.0
invariant: 2.2.4
prop-types: 15.8.1
dequal@2.0.3: {}
destr@2.0.5: {}
@@ -21054,10 +21086,6 @@ snapshots:
es-errors: 1.3.0
get-intrinsic: 1.3.0
get-tsconfig@4.10.0:
dependencies:
resolve-pkg-maps: 1.0.0
get-tsconfig@4.10.1:
dependencies:
resolve-pkg-maps: 1.0.0
@@ -21545,7 +21573,7 @@ snapshots:
jiti: 2.0.0-beta.3
jiti-v1: jiti@1.21.7
pathe: 1.1.2
tsx: 4.20.3
tsx: 4.20.6
transitivePeerDependencies:
- supports-color
@@ -22724,7 +22752,7 @@ snapshots:
metro-transform-plugins@0.83.3:
dependencies:
'@babel/core': 7.28.4
'@babel/generator': 7.28.3
'@babel/generator': 7.28.5
'@babel/template': 7.27.2
'@babel/traverse': 7.28.4
flow-enums-runtime: 0.0.6
@@ -22755,7 +22783,7 @@ snapshots:
metro-transform-worker@0.83.3:
dependencies:
'@babel/core': 7.28.4
'@babel/generator': 7.28.3
'@babel/generator': 7.28.5
'@babel/parser': 7.28.5
'@babel/types': 7.28.5
flow-enums-runtime: 0.0.6
@@ -24019,6 +24047,13 @@ snapshots:
react-is@19.2.0: {}
react-native-blob-util@0.23.2(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
base-64: 0.1.0
glob: 10.4.5
react: 19.1.0
react-native: 0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0)
react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
'@egjs/hammerjs': 2.0.17
@@ -24032,6 +24067,14 @@ snapshots:
react: 19.1.0
react-native: 0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0)
react-native-pdf@7.0.3(react-native-blob-util@0.23.2(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
crypto-js: 4.2.0
deprecated-react-native-prop-types: 2.3.0
react: 19.1.0
react-native: 0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0)
react-native-blob-util: 0.23.2(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-reanimated@4.1.5(@babel/core@7.28.4)(react-native-worklets@0.5.1(@babel/core@7.28.4)(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.3))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
'@babel/core': 7.28.4
@@ -24522,7 +24565,7 @@ snapshots:
rolldown-plugin-dts@0.15.6(rolldown@1.0.0-beta.31)(typescript@5.9.3):
dependencies:
'@babel/generator': 7.28.3
'@babel/generator': 7.28.5
'@babel/parser': 7.28.5
'@babel/types': 7.28.5
ast-kit: 2.1.1
@@ -25563,13 +25606,6 @@ snapshots:
tslib@2.8.1: {}
tsx@4.20.3:
dependencies:
esbuild: 0.25.10
get-tsconfig: 4.10.1
optionalDependencies:
fsevents: 2.3.3
tsx@4.20.6:
dependencies:
esbuild: 0.25.10