diff --git a/.changeset/bright-toes-share.md b/.changeset/bright-toes-share.md new file mode 100644 index 00000000..79099242 --- /dev/null +++ b/.changeset/bright-toes-share.md @@ -0,0 +1,12 @@ +--- +"@rozenite/storage-plugin": minor +--- + +Introduce `@rozenite/storage-plugin` as a generic storage inspector for React Native devtools. + +User-facing changes: +- Add `useRozeniteStoragePlugin({ storages })` API for registering one or more adapters. +- Support named storages across adapters so multiple independent stores can be inspected in a single plugin panel. +- Provide built-in adapters for MMKV, AsyncStorage (including v2 and v3-style usage), and Expo SecureStore. +- Improve entry workflows in the panel by prefilling the key when an entry is selected, making update/delete actions faster. +- Add official documentation for the new Storage plugin and guide users from MMKV docs toward the generic plugin path. diff --git a/apps/playground/package.json b/apps/playground/package.json index b9bc65d9..450c70a2 100644 --- a/apps/playground/package.json +++ b/apps/playground/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@expo/vector-icons": "^15.0.3", + "@react-native-async-storage/async-storage": "^3.0.1", "@react-navigation/bottom-tabs": "^7.4.7", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.17", @@ -28,6 +29,7 @@ "@rozenite/react-navigation-plugin": "workspace:*", "@rozenite/redux-devtools-plugin": "workspace:*", "@rozenite/require-profiler-plugin": "workspace:*", + "@rozenite/storage-plugin": "workspace:*", "@rozenite/tanstack-query-plugin": "workspace:*", "@rozenite/web": "workspace:*", "@tanstack/react-query": "^5.81.5", @@ -38,6 +40,7 @@ "expo-image": "~55.0.3", "expo-linking": "~55.0.4", "expo-router": "~55.0.0-preview.7", + "expo-secure-store": "^55.0.8", "expo-splash-screen": "~55.0.5", "expo-status-bar": "~55.0.2", "expo-symbols": "~55.0.3", diff --git a/apps/playground/src/app/App.tsx b/apps/playground/src/app/App.tsx index 16ff4ce7..ea638e1b 100644 --- a/apps/playground/src/app/App.tsx +++ b/apps/playground/src/app/App.tsx @@ -7,6 +7,7 @@ import { useMMKVDevTools } from '@rozenite/mmkv-plugin'; import { useNetworkActivityDevTools } from '@rozenite/network-activity-plugin'; import { usePerformanceMonitorDevTools } from '@rozenite/performance-monitor-plugin'; import { useReactNavigationDevTools } from '@rozenite/react-navigation-plugin'; +import { useRozeniteStoragePlugin } from '@rozenite/storage-plugin'; import { useTanStackQueryDevTools } from '@rozenite/tanstack-query-plugin'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useRef } from 'react'; @@ -25,6 +26,8 @@ import { PerformanceMonitorScreen } from './screens/PerformanceMonitorScreen'; import { ReduxTestScreen } from './screens/ReduxTestScreen'; import { RequestBodyTestScreen } from './screens/RequestBodyTestScreen'; import { RequireProfilerTestScreen } from './screens/RequireProfilerTestScreen'; +import { StoragePluginScreen } from './screens/StoragePluginScreen'; +import { storagePluginAdapters } from './storage-plugin-adapters'; import { primaryStore } from './store'; import { useRequireProfilerDevTools } from '@rozenite/require-profiler-plugin'; import { withOnBootNetworkActivityRecording } from '@rozenite/network-activity-plugin'; @@ -47,6 +50,9 @@ const Wrapper = () => { storages: mmkvStorages, blacklist: /user-storage:sensitiveToken/, }); + useRozeniteStoragePlugin({ + storages: storagePluginAdapters, + }); usePerformanceMonitorDevTools(); useRequireProfilerDevTools(); @@ -60,6 +66,7 @@ const Wrapper = () => { > + @@ -104,6 +111,7 @@ const linking = { screens: { Landing: '', MMKVPlugin: 'mmkv', + StoragePlugin: 'storage', NetworkTest: 'network', ReduxTest: 'redux', PerformanceMonitor: 'performance', diff --git a/apps/playground/src/app/navigation/types.ts b/apps/playground/src/app/navigation/types.ts index 4a11d877..1d23b18a 100644 --- a/apps/playground/src/app/navigation/types.ts +++ b/apps/playground/src/app/navigation/types.ts @@ -3,6 +3,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'; export type RootStackParamList = { Landing: undefined; MMKVPlugin: undefined; + StoragePlugin: undefined; NetworkTest: undefined; RequestBodyTest: undefined; ReduxTest: undefined; diff --git a/apps/playground/src/app/screens/LandingScreen.tsx b/apps/playground/src/app/screens/LandingScreen.tsx index 79cbb8d6..9c98f3f3 100644 --- a/apps/playground/src/app/screens/LandingScreen.tsx +++ b/apps/playground/src/app/screens/LandingScreen.tsx @@ -30,6 +30,13 @@ export const LandingScreen = () => { MMKV Plugin + navigation.navigate('StoragePlugin' as never)} + > + Storage Plugin + + navigation.navigate('NetworkTest' as never)} diff --git a/apps/playground/src/app/screens/StoragePluginScreen.tsx b/apps/playground/src/app/screens/StoragePluginScreen.tsx new file mode 100644 index 00000000..95e778df --- /dev/null +++ b/apps/playground/src/app/screens/StoragePluginScreen.tsx @@ -0,0 +1,525 @@ +import { useEffect, useMemo, useState } from 'react'; +import { + Alert, + FlatList, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; +import * as SecureStore from 'expo-secure-store'; +import { MMKV } from 'react-native-mmkv'; +import { initializeMMKVStorages, mmkvStorages } from '../mmkv-storages'; +import { + asyncStorageV2, + asyncStorageV3Instances, + forgetSecureStoreKey, + getKnownSecureStoreKeys, + rememberSecureStoreKey, +} from '../storage-plugin-adapters'; + +type AdapterTab = 'mmkv' | 'async' | 'secure'; +type AsyncStorageMode = 'v2-default' | 'v3-auth' | 'v3-cache'; +type EntryType = 'string' | 'number' | 'boolean' | 'buffer'; +type AsyncStorageLike = { + getAllKeys: () => Promise; + getItem: (key: string) => Promise; + setItem: (key: string, value: string) => Promise; + removeItem: (key: string) => Promise; +}; + +type Entry = { + key: string; + value: string; + type: EntryType; +}; + +const mmkvIds = Object.keys(mmkvStorages) as Array; + +const parseMMKVEntry = (storage: MMKV, key: string): Entry | null => { + const stringValue = storage.getString(key); + if (stringValue !== undefined) { + return { key, type: 'string', value: stringValue }; + } + + const numberValue = storage.getNumber(key); + if (numberValue !== undefined) { + return { key, type: 'number', value: String(numberValue) }; + } + + const booleanValue = storage.getBoolean(key); + if (booleanValue !== undefined) { + return { key, type: 'boolean', value: String(booleanValue) }; + } + + const bufferValue = storage.getBuffer(key); + if (bufferValue !== undefined) { + return { + key, + type: 'buffer', + value: JSON.stringify(Array.from(new Uint8Array(bufferValue))), + }; + } + + return null; +}; + +export const StoragePluginScreen = () => { + const [tab, setTab] = useState('mmkv'); + const [asyncStorageMode, setAsyncStorageMode] = + useState('v2-default'); + const [mmkvStorageId, setMmkvStorageId] = useState('user-storage'); + const [key, setKey] = useState(''); + const [value, setValue] = useState(''); + const [entryType, setEntryType] = useState('string'); + const [entries, setEntries] = useState([]); + + useEffect(() => { + initializeMMKVStorages(); + }, []); + + const supportsTypedValues = tab === 'mmkv'; + + const title = useMemo(() => { + if (tab === 'mmkv') { + return 'MMKV Adapter'; + } + + if (tab === 'async') { + return 'AsyncStorage Adapter'; + } + + return 'SecureStore Adapter'; + }, [tab]); + + const selectedAsyncStorage: AsyncStorageLike = useMemo(() => { + if (asyncStorageMode === 'v3-auth') { + return asyncStorageV3Instances.auth; + } + + if (asyncStorageMode === 'v3-cache') { + return asyncStorageV3Instances.cache; + } + + return asyncStorageV2; + }, [asyncStorageMode]); + + const loadEntries = async () => { + if (tab === 'mmkv') { + const storage = mmkvStorages[mmkvStorageId]; + const nextEntries = storage + .getAllKeys() + .map((entryKey) => parseMMKVEntry(storage, entryKey)) + .filter((item): item is Entry => !!item); + setEntries(nextEntries); + return; + } + + if (tab === 'async') { + const keys = await selectedAsyncStorage.getAllKeys(); + const values = await Promise.all( + keys.map(async (entryKey) => [ + entryKey, + await selectedAsyncStorage.getItem(entryKey), + ] as const) + ); + setEntries( + values.map(([entryKey, entryValue]) => ({ + key: entryKey, + type: 'string', + value: entryValue ?? '', + })) + ); + return; + } + + const keys = getKnownSecureStoreKeys(); + const values = await Promise.all( + keys.map(async (entryKey) => ({ + key: entryKey, + value: (await SecureStore.getItemAsync(entryKey)) ?? '', + })) + ); + + setEntries( + values + .filter((item) => item.value !== '') + .map((item) => ({ + key: item.key, + type: 'string', + value: item.value, + })) + ); + }; + + useEffect(() => { + void loadEntries(); + }, [tab, mmkvStorageId, asyncStorageMode]); + + const handleSet = async () => { + if (!key.trim()) { + return; + } + + try { + if (tab === 'mmkv') { + const storage = mmkvStorages[mmkvStorageId]; + if (entryType === 'string') { + storage.set(key, value); + } else if (entryType === 'number') { + const parsed = Number(value); + if (Number.isNaN(parsed)) { + throw new Error('Invalid number value'); + } + storage.set(key, parsed); + } else if (entryType === 'boolean') { + if (value !== 'true' && value !== 'false') { + throw new Error('Boolean value must be true or false'); + } + storage.set(key, value === 'true'); + } else { + const parsed = JSON.parse(value); + if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === 'number')) { + throw new Error('Buffer value must be a JSON array of numbers'); + } + storage.set(key, new Uint8Array(parsed).buffer); + } + } else if (tab === 'async') { + await selectedAsyncStorage.setItem(key, value); + } else { + rememberSecureStoreKey(key); + await SecureStore.setItemAsync(key, value); + } + + setValue(''); + await loadEntries(); + } catch (error) { + Alert.alert('Set failed', error instanceof Error ? error.message : 'Unknown error'); + } + }; + + const handleDelete = async () => { + if (!key.trim()) { + return; + } + + if (tab === 'mmkv') { + const storage = mmkvStorages[mmkvStorageId] as unknown as { + delete?: (entryKey: string) => void; + remove?: (entryKey: string) => void; + }; + if (typeof storage.remove === 'function') { + storage.remove(key); + } else { + storage.delete?.(key); + } + } else if (tab === 'async') { + await selectedAsyncStorage.removeItem(key); + } else { + forgetSecureStoreKey(key); + await SecureStore.deleteItemAsync(key); + } + + setValue(''); + await loadEntries(); + }; + + const handleSelectEntry = (entry: Entry) => { + setKey(entry.key); + setValue(entry.value); + + if (supportsTypedValues) { + setEntryType(entry.type); + } else { + setEntryType('string'); + } + }; + + return ( + + Storage Plugin Testbed + + setTab('mmkv')} + > + MMKV + + setTab('async')} + > + Async + + setTab('secure')} + > + Secure + + + + {title} + + {tab === 'mmkv' && ( + + {mmkvIds.map((id) => ( + setMmkvStorageId(id)} + > + {id} + + ))} + + )} + + {tab === 'async' && ( + + setAsyncStorageMode('v2-default')} + > + v2 default + + setAsyncStorageMode('v3-auth')} + > + v3 auth + + setAsyncStorageMode('v3-cache')} + > + v3 cache + + + )} + + + + + {(['string', 'number', 'boolean', 'buffer'] as EntryType[]).map((type) => { + const disabled = !supportsTypedValues && type !== 'string'; + return ( + setEntryType(type)} + style={[ + styles.typeChip, + entryType === type && styles.typeChipActive, + disabled && styles.typeChipDisabled, + ]} + > + {type} + + ); + })} + + + {!supportsTypedValues && ( + + AsyncStorage and SecureStore are string-only adapters in this demo. + + )} + + + + + void handleSet()}> + Set + + void handleDelete()} + > + Delete + + void loadEntries()}> + Refresh + + + + `${item.key}:${item.type}`} + renderItem={({ item }) => ( + handleSelectEntry(item)} + activeOpacity={0.8} + > + {item.key} + {item.type} + + {item.value} + + + )} + ListEmptyComponent={No entries} + /> + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#0a0a0a', + paddingHorizontal: 16, + paddingTop: 24, + gap: 10, + }, + title: { + color: '#ffffff', + fontSize: 22, + fontWeight: '700', + }, + tabsRow: { + flexDirection: 'row', + gap: 8, + }, + tabButton: { + flex: 1, + alignItems: 'center', + paddingVertical: 10, + borderRadius: 10, + backgroundColor: '#1f2937', + }, + tabButtonActive: { + backgroundColor: '#8232FF', + }, + tabButtonText: { + color: '#ffffff', + fontWeight: '600', + }, + sectionTitle: { + color: '#d1d5db', + fontSize: 14, + fontWeight: '600', + }, + inlineRow: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 8, + }, + storageChip: { + backgroundColor: '#1f2937', + borderRadius: 999, + paddingHorizontal: 12, + paddingVertical: 8, + }, + storageChipActive: { + backgroundColor: '#374151', + borderWidth: 1, + borderColor: '#8232FF', + }, + storageChipText: { + color: '#e5e7eb', + fontSize: 12, + }, + typeChip: { + backgroundColor: '#1f2937', + borderRadius: 8, + paddingHorizontal: 10, + paddingVertical: 6, + }, + typeChipActive: { + backgroundColor: '#8232FF', + }, + typeChipDisabled: { + opacity: 0.45, + }, + typeChipText: { + color: '#f9fafb', + fontSize: 12, + }, + note: { + color: '#fbbf24', + fontSize: 12, + }, + input: { + backgroundColor: '#111827', + borderWidth: 1, + borderColor: '#374151', + borderRadius: 10, + color: '#ffffff', + paddingHorizontal: 12, + paddingVertical: 10, + }, + actionsRow: { + flexDirection: 'row', + gap: 8, + }, + actionButton: { + flex: 1, + backgroundColor: '#2563eb', + borderRadius: 8, + alignItems: 'center', + paddingVertical: 10, + }, + deleteButton: { + backgroundColor: '#dc2626', + }, + actionButtonText: { + color: '#ffffff', + fontWeight: '600', + }, + entryRow: { + backgroundColor: '#111827', + borderWidth: 1, + borderColor: '#1f2937', + borderRadius: 8, + padding: 10, + marginBottom: 8, + gap: 4, + }, + entryKey: { + color: '#f9fafb', + fontWeight: '700', + }, + entryType: { + color: '#a78bfa', + fontSize: 12, + }, + entryValue: { + color: '#93c5fd', + }, + emptyState: { + color: '#6b7280', + textAlign: 'center', + marginTop: 24, + }, +}); diff --git a/apps/playground/src/app/storage-plugin-adapters.ts b/apps/playground/src/app/storage-plugin-adapters.ts new file mode 100644 index 00000000..435ba721 --- /dev/null +++ b/apps/playground/src/app/storage-plugin-adapters.ts @@ -0,0 +1,69 @@ +import AsyncStorage, { + createAsyncStorage, +} from '@react-native-async-storage/async-storage'; +import * as SecureStore from 'expo-secure-store'; +import { + createAsyncStorageAdapter, + createExpoSecureStorageAdapter, + createMMKVStorageAdapter, +} from '@rozenite/storage-plugin'; +import { mmkvStorages } from './mmkv-storages'; + +export const asyncStorageV2 = AsyncStorage; +export const asyncStorageV3Instances = { + auth: createAsyncStorage('rozenite-playground-auth'), + cache: createAsyncStorage('rozenite-playground-cache'), +}; + +const secureStoreKnownKeys = new Set(['token', 'session']); + +export const rememberSecureStoreKey = (key: string) => { + if (!key.trim()) { + return; + } + + secureStoreKnownKeys.add(key.trim()); +}; + +export const forgetSecureStoreKey = (key: string) => { + secureStoreKnownKeys.delete(key); +}; + +export const getKnownSecureStoreKeys = () => [...secureStoreKnownKeys.values()]; + +export const storagePluginAdapters = [ + createMMKVStorageAdapter({ + adapterId: 'mmkv', + adapterName: 'MMKV', + storages: mmkvStorages, + blacklist: { + 'user-storage': /sensitiveToken/, + }, + }), + createAsyncStorageAdapter({ + storages: { + 'v2-default': { + storage: asyncStorageV2, + name: 'AsyncStorage v2 (default)', + }, + 'v3-auth': { + storage: asyncStorageV3Instances.auth, + name: 'AsyncStorage v3 (auth)', + }, + 'v3-cache': { + storage: asyncStorageV3Instances.cache, + name: 'AsyncStorage v3 (cache)', + }, + }, + adapterId: 'async-storage', + adapterName: 'AsyncStorage', + }), + createExpoSecureStorageAdapter({ + storage: SecureStore, + keys: async () => getKnownSecureStoreKeys(), + adapterId: 'secure-store', + adapterName: 'Expo SecureStore', + storageId: 'secure-default', + storageName: 'Default SecureStore', + }), +]; diff --git a/commitlint.config.js b/commitlint.config.js index 86ba0705..4afcf879 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -26,7 +26,8 @@ export default { 'overlay-plugin', 'chrome-extension', 'web', - '', + 'storage-plugin', + '' ], ], }, diff --git a/packages/storage-plugin/CHANGELOG.md b/packages/storage-plugin/CHANGELOG.md new file mode 100644 index 00000000..2e0fe229 --- /dev/null +++ b/packages/storage-plugin/CHANGELOG.md @@ -0,0 +1,10 @@ +# @rozenite/storage-plugin + +## 0.1.0 + +- Initial release. +- Added generic storage plugin with sync/async adapters. +- Added MMKV, AsyncStorage and Expo SecureStore adapter factories. +- Added capabilities-aware UI and runtime validation. +- Added per-storage blacklist support. +- Added MCP tools for listing, reading and mutating entries. diff --git a/packages/storage-plugin/README.md b/packages/storage-plugin/README.md new file mode 100644 index 00000000..f881c91e --- /dev/null +++ b/packages/storage-plugin/README.md @@ -0,0 +1,77 @@ +![rozenite-banner](https://www.rozenite.dev/rozenite-banner.jpg) + +### A Rozenite plugin for inspecting multiple storage backends in React Native DevTools. + +The Storage Plugin provides a single inspector for sync and async storages, including MMKV, AsyncStorage and Expo SecureStore via adapters. + +## Installation + +```bash +npm install @rozenite/storage-plugin +``` + +Optional peers depending on adapters you use: + +```bash +npm install react-native-mmkv @react-native-async-storage/async-storage expo-secure-store +``` + +## Usage + +```ts +import { + createAsyncStorageAdapter, + createMMKVStorageAdapter, + createExpoSecureStorageAdapter, + useRozeniteStoragePlugin, +} from '@rozenite/storage-plugin'; + +const storages = [ + createMMKVStorageAdapter({ + storages: { + user: userStorage, + cache: cacheStorage, + }, + }), + createAsyncStorageAdapter({ + storage: AsyncStorage, + }), + createExpoSecureStorageAdapter({ + storage: SecureStore, + keys: ['token', 'session'], + }), +]; + +useRozeniteStoragePlugin({ storages }); +``` + +### AsyncStorage v2 and v3 + +`createAsyncStorageAdapter` supports both: + +```ts +// v2 style: single storage object +createAsyncStorageAdapter({ + storage: AsyncStorage, +}); + +// v3 style: named storage instances +createAsyncStorageAdapter({ + storages: { + auth: authStorageInstance, + cache: { + storage: cacheStorageInstance, + name: 'Cache Instance', + }, + }, +}); +``` + +`createExpoAsyncStorageAdapter` is still exported as a backward-compatible alias. + +## Notes + +- Unsupported value types are disabled in UI create/edit flows. +- Type support is enforced in UI, runtime and MCP tools. +- Storages without subscriptions automatically use internal polling updates. +- Per-storage blacklists are supported through adapter configuration. diff --git a/packages/storage-plugin/package.json b/packages/storage-plugin/package.json new file mode 100644 index 00000000..c12656b5 --- /dev/null +++ b/packages/storage-plugin/package.json @@ -0,0 +1,59 @@ +{ + "name": "@rozenite/storage-plugin", + "version": "0.1.0", + "description": "Generic Storage Inspector for Rozenite.", + "type": "module", + "main": "./dist/react-native.cjs", + "module": "./dist/react-native.js", + "types": "./dist/react-native.d.ts", + "scripts": { + "build": "rozenite build", + "dev": "rozenite dev", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "eslint ." + }, + "dependencies": { + "@rozenite/plugin-bridge": "workspace:*" + }, + "devDependencies": { + "@rozenite/vite-plugin": "workspace:*", + "@tanstack/react-table": "^8.21.3", + "@types/react": "catalog:", + "autoprefixer": "^10.4.21", + "lucide-react": "^0.263.1", + "postcss": "^8.5.6", + "react": "catalog:", + "react-dom": "catalog:", + "react-json-tree": "^0.20.0", + "react-native": "catalog:", + "react-native-mmkv": "^3.3.0", + "react-native-mmkv-v3": "npm:react-native-mmkv@^3.0.0", + "react-native-mmkv-v4": "npm:react-native-mmkv@^4.0.0", + "react-native-nitro-modules": "*", + "react-native-web": "^0.21.2", + "rozenite": "workspace:*", + "tailwindcss": "^3.4.17", + "tailwindcss-animate": "^1.0.7", + "typescript": "~5.9.3", + "vite": "catalog:" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "*", + "expo-secure-store": "*", + "react": "*", + "react-native": "*", + "react-native-mmkv": "*" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + }, + "expo-secure-store": { + "optional": true + }, + "react-native-mmkv": { + "optional": true + } + }, + "license": "MIT" +} diff --git a/packages/storage-plugin/postcss.config.js b/packages/storage-plugin/postcss.config.js new file mode 100644 index 00000000..2aa7205d --- /dev/null +++ b/packages/storage-plugin/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/packages/storage-plugin/react-native.ts b/packages/storage-plugin/react-native.ts new file mode 100644 index 00000000..06397164 --- /dev/null +++ b/packages/storage-plugin/react-native.ts @@ -0,0 +1,40 @@ +export { + createAsyncStorageAdapter, + createExpoAsyncStorageAdapter, + createExpoSecureStorageAdapter, + createMMKVStorageAdapter, +} from './src/react-native/adapters'; +export type { + AsyncStorageInstanceConfig, + AsyncStorageLike, + CreateAsyncStorageAdapterOptions, + CreateExpoAsyncStorageAdapterOptions, + CreateExpoSecureStorageAdapterOptions, + CreateMMKVStorageAdapterOptions, + SecureStorageLike, +} from './src/react-native/adapters'; + +export type { + AsyncStorage, + StorageAdapter, + StorageCapabilities, + StorageEntry, + StorageEntryType, + StorageEntryValue, + StorageNode, + SyncStorage, +} from './src/shared/types'; + +export let useRozeniteStoragePlugin: typeof import('./src/react-native/useRozeniteStoragePlugin').useRozeniteStoragePlugin; + +const isWeb = + typeof window !== 'undefined' && window.navigator.product !== 'ReactNative'; +const isDev = process.env.NODE_ENV !== 'production'; +const isServer = typeof window === 'undefined'; + +if (isDev && !isWeb && !isServer) { + useRozeniteStoragePlugin = + require('./src/react-native/useRozeniteStoragePlugin').useRozeniteStoragePlugin; +} else { + useRozeniteStoragePlugin = () => null; +} diff --git a/packages/storage-plugin/rozenite.config.ts b/packages/storage-plugin/rozenite.config.ts new file mode 100644 index 00000000..9b6e9d63 --- /dev/null +++ b/packages/storage-plugin/rozenite.config.ts @@ -0,0 +1,8 @@ +export default { + panels: [ + { + name: 'Storage', + source: './src/ui/panel.tsx', + }, + ], +}; diff --git a/packages/storage-plugin/src/css-modules.d.ts b/packages/storage-plugin/src/css-modules.d.ts new file mode 100644 index 00000000..3d673e2e --- /dev/null +++ b/packages/storage-plugin/src/css-modules.d.ts @@ -0,0 +1,4 @@ +declare module '*.module.css' { + const classes: { [key: string]: string }; + export default classes; +} diff --git a/packages/storage-plugin/src/react-native/adapters/async-storage.ts b/packages/storage-plugin/src/react-native/adapters/async-storage.ts new file mode 100644 index 00000000..9698fe95 --- /dev/null +++ b/packages/storage-plugin/src/react-native/adapters/async-storage.ts @@ -0,0 +1,115 @@ +import type { StorageAdapter, StorageNode } from '../../shared/types'; + +export type AsyncStorageLike = { + getAllKeys: () => Promise; + getItem: (key: string) => Promise; + setItem: (key: string, value: string) => Promise; + removeItem: (key: string) => Promise; +}; + +export type AsyncStorageInstanceConfig = { + storage: AsyncStorageLike; + name?: string; + blacklist?: RegExp; +}; + +type SingleStorageOptions = { + storage: AsyncStorageLike; + adapterId?: string; + adapterName?: string; + storageId?: string; + storageName?: string; + blacklist?: RegExp; +}; + +type MultiStorageOptions = { + storages: Record; + adapterId?: string; + adapterName?: string; +}; + +export type CreateAsyncStorageAdapterOptions = + | SingleStorageOptions + | MultiStorageOptions; + +const toStorageNode = ( + storageId: string, + config: AsyncStorageLike | AsyncStorageInstanceConfig, + fallbackName?: string, + fallbackBlacklist?: RegExp +): StorageNode => { + const resolved = + 'storage' in config + ? config + : { + storage: config, + }; + + const { storage, name, blacklist } = resolved; + + return { + id: storageId, + name: name ?? fallbackName ?? storageId, + blacklist: blacklist ?? fallbackBlacklist, + capabilities: { + supportedTypes: ['string'], + }, + storage: { + kind: 'async', + getAllKeys: () => storage.getAllKeys(), + get: async (key) => { + const value = await storage.getItem(key); + if (value === null) { + return undefined; + } + + return { + key, + type: 'string', + value, + }; + }, + set: async (entry) => { + if (entry.type !== 'string') { + throw new Error('AsyncStorage adapter supports only string values.'); + } + + await storage.setItem(entry.key, entry.value); + }, + delete: (key) => storage.removeItem(key), + }, + }; +}; + +export const createAsyncStorageAdapter = ( + options: CreateAsyncStorageAdapterOptions +): StorageAdapter => { + const { adapterId = 'async-storage', adapterName = 'AsyncStorage' } = + options; + + const storageNodes: StorageNode[] = + 'storages' in options + ? Object.entries(options.storages).map(([storageId, config]) => + toStorageNode(storageId, config) + ) + : [ + toStorageNode( + options.storageId ?? 'default', + options.storage, + options.storageName ?? 'Default Storage', + options.blacklist + ), + ]; + + return { + id: adapterId, + name: adapterName, + storages: storageNodes, + }; +}; + +/** + * @deprecated Use createAsyncStorageAdapter instead. + */ +export const createExpoAsyncStorageAdapter = createAsyncStorageAdapter; +export type CreateExpoAsyncStorageAdapterOptions = CreateAsyncStorageAdapterOptions; diff --git a/packages/storage-plugin/src/react-native/adapters/index.ts b/packages/storage-plugin/src/react-native/adapters/index.ts new file mode 100644 index 00000000..60e66dbb --- /dev/null +++ b/packages/storage-plugin/src/react-native/adapters/index.ts @@ -0,0 +1,19 @@ +export { createMMKVStorageAdapter } from './mmkv'; +export type { CreateMMKVStorageAdapterOptions } from './mmkv'; + +export { + createAsyncStorageAdapter, + createExpoAsyncStorageAdapter, +} from './async-storage'; +export type { + AsyncStorageInstanceConfig, + AsyncStorageLike, + CreateAsyncStorageAdapterOptions, + CreateExpoAsyncStorageAdapterOptions, +} from './async-storage'; + +export { createExpoSecureStorageAdapter } from './secure-storage'; +export type { + CreateExpoSecureStorageAdapterOptions, + SecureStorageLike, +} from './secure-storage'; diff --git a/packages/storage-plugin/src/react-native/adapters/mmkv.ts b/packages/storage-plugin/src/react-native/adapters/mmkv.ts new file mode 100644 index 00000000..d61d23c9 --- /dev/null +++ b/packages/storage-plugin/src/react-native/adapters/mmkv.ts @@ -0,0 +1,187 @@ +import type { MMKV as MMKVV3 } from 'react-native-mmkv-v3'; +import type { MMKV as MMKVV4 } from 'react-native-mmkv-v4'; +import type { StorageAdapter, StorageEntry, StorageNode } from '../../shared/types'; +import { DEFAULT_SUPPORTED_TYPES } from '../../shared/types'; +import { looksLikeGarbled } from '../is-garbled'; + +type MMKV = MMKVV3 | MMKVV4; + +type MMKVAdapter = { + set: (key: string, value: boolean | string | number | ArrayBuffer) => void; + getBoolean: (key: string) => boolean | undefined; + getString: (key: string) => string | undefined; + getNumber: (key: string) => number | undefined; + getBuffer: (key: string) => ArrayBuffer | undefined; + delete: (key: string) => void; + getAllKeys: () => string[]; + addOnValueChangedListener: (callback: (key: string) => void) => { + remove: () => void; + }; +}; + +export type MMKVBlacklistConfig = RegExp | Record; + +export type CreateMMKVStorageAdapterOptions = { + adapterId?: string; + adapterName?: string; + storages: MMKV[] | Record; + blacklist?: MMKVBlacklistConfig; +}; + +const isMMKVV4 = (mmkv: MMKV): mmkv is MMKVV4 => 'remove' in mmkv; + +const normalizeStorages = (storages: MMKV[] | Record) => { + if (Array.isArray(storages)) { + const isAnyStorageV4 = storages.some(isMMKVV4); + + if (isAnyStorageV4) { + throw new Error( + '[Rozenite] Storage Plugin: MMKV arrays are not supported for v4 storages. Pass a record of storage IDs and MMKV instances.' + ); + } + + return Object.fromEntries( + (storages as MMKVV3[]).map((storage) => [storage['id'], storage]) + ); + } + + return storages; +}; + +const getMMKVAdapter = (mmkv: MMKV): MMKVAdapter => { + if (isMMKVV4(mmkv)) { + return { + set: (key, value) => mmkv.set(key, value), + getBoolean: (key) => mmkv.getBoolean(key), + getString: (key) => mmkv.getString(key), + getNumber: (key) => mmkv.getNumber(key), + getBuffer: (key) => mmkv.getBuffer(key), + delete: (key) => mmkv.remove(key), + getAllKeys: () => mmkv.getAllKeys(), + addOnValueChangedListener: (callback) => + mmkv.addOnValueChangedListener(callback), + }; + } + + return { + set: (key, value) => mmkv.set(key, value), + getBoolean: (key) => mmkv.getBoolean(key), + getString: (key) => mmkv.getString(key), + getNumber: (key) => mmkv.getNumber(key), + getBuffer: (key) => mmkv.getBuffer(key) as ArrayBuffer | undefined, + delete: (key) => mmkv.delete(key), + getAllKeys: () => mmkv.getAllKeys(), + addOnValueChangedListener: (callback) => mmkv.addOnValueChangedListener(callback), + }; +}; + +const getEntry = (adapter: MMKVAdapter, key: string): StorageEntry | undefined => { + const stringValue = adapter.getString(key); + + if (stringValue !== undefined && stringValue.length > 0) { + if (looksLikeGarbled(stringValue)) { + return { + key, + type: 'buffer', + value: Array.from(new TextEncoder().encode(stringValue)), + }; + } + + return { + key, + type: 'string', + value: stringValue, + }; + } + + const numberValue = adapter.getNumber(key); + if (numberValue !== undefined) { + return { + key, + type: 'number', + value: numberValue, + }; + } + + const booleanValue = adapter.getBoolean(key); + if (booleanValue !== undefined) { + return { + key, + type: 'boolean', + value: booleanValue, + }; + } + + const bufferValue = adapter.getBuffer(key); + if (bufferValue !== undefined) { + return { + key, + type: 'buffer', + value: Array.from(new Uint8Array(bufferValue)), + }; + } + + return undefined; +}; + +const setEntry = (adapter: MMKVAdapter, entry: StorageEntry) => { + if (entry.type === 'buffer') { + adapter.set(entry.key, new Uint8Array(entry.value).buffer); + return; + } + + adapter.set(entry.key, entry.value); +}; + +const getStorageBlacklist = ( + config: MMKVBlacklistConfig | undefined, + storageId: string +) => { + if (!config) { + return undefined; + } + + if (config instanceof RegExp) { + return config; + } + + return config[storageId]; +}; + +export const createMMKVStorageAdapter = ({ + adapterId = 'mmkv', + adapterName = 'MMKV', + storages, + blacklist, +}: CreateMMKVStorageAdapterOptions): StorageAdapter => { + const normalizedStorages = normalizeStorages(storages) as Record; + + const storageNodes: StorageNode[] = Object.entries(normalizedStorages).map( + ([storageId, storage]) => { + const mmkv = getMMKVAdapter(storage); + + return { + id: storageId, + name: storageId, + blacklist: getStorageBlacklist(blacklist, storageId), + capabilities: { + supportedTypes: DEFAULT_SUPPORTED_TYPES, + }, + storage: { + kind: 'sync', + getAllKeys: () => mmkv.getAllKeys(), + get: (key) => getEntry(mmkv, key), + set: (entry) => setEntry(mmkv, entry), + delete: (key) => mmkv.delete(key), + subscribe: (callback) => mmkv.addOnValueChangedListener(callback), + }, + }; + } + ); + + return { + id: adapterId, + name: adapterName, + storages: storageNodes, + }; +}; diff --git a/packages/storage-plugin/src/react-native/adapters/secure-storage.ts b/packages/storage-plugin/src/react-native/adapters/secure-storage.ts new file mode 100644 index 00000000..cdff80bb --- /dev/null +++ b/packages/storage-plugin/src/react-native/adapters/secure-storage.ts @@ -0,0 +1,76 @@ +import type { StorageAdapter, StorageNode } from '../../shared/types'; + +export type SecureStorageLike = { + getItemAsync: (key: string) => Promise; + setItemAsync: (key: string, value: string) => Promise; + deleteItemAsync: (key: string) => Promise; +}; + +type KeySource = string[] | (() => Promise); + +export type CreateExpoSecureStorageAdapterOptions = { + storage: SecureStorageLike; + keys: KeySource; + adapterId?: string; + adapterName?: string; + storageId?: string; + storageName?: string; + blacklist?: RegExp; +}; + +const resolveKeys = async (source: KeySource) => { + if (Array.isArray(source)) { + return source; + } + + return source(); +}; + +export const createExpoSecureStorageAdapter = ({ + storage, + keys, + adapterId = 'expo-secure-store', + adapterName = 'Expo SecureStore', + storageId = 'default', + storageName = 'Default Secure Storage', + blacklist, +}: CreateExpoSecureStorageAdapterOptions): StorageAdapter => { + const storageNode: StorageNode = { + id: storageId, + name: storageName, + blacklist, + capabilities: { + supportedTypes: ['string'], + }, + storage: { + kind: 'async', + getAllKeys: () => resolveKeys(keys), + get: async (key) => { + const value = await storage.getItemAsync(key); + if (value === null) { + return undefined; + } + + return { + key, + type: 'string', + value, + }; + }, + set: async (entry) => { + if (entry.type !== 'string') { + throw new Error('Expo SecureStore adapter supports only string values.'); + } + + await storage.setItemAsync(entry.key, entry.value); + }, + delete: (key) => storage.deleteItemAsync(key), + }, + }; + + return { + id: adapterId, + name: adapterName, + storages: [storageNode], + }; +}; diff --git a/packages/storage-plugin/src/react-native/is-garbled.ts b/packages/storage-plugin/src/react-native/is-garbled.ts new file mode 100644 index 00000000..b986c802 --- /dev/null +++ b/packages/storage-plugin/src/react-native/is-garbled.ts @@ -0,0 +1,17 @@ +// This is a heuristic to determine if a string is garbled. +export const looksLikeGarbled = (str: string): boolean => { + // 1. Check for replacement character (�) + if (str.includes('\uFFFD')) return true; + + // 2. Check for unusual control characters + // eslint-disable-next-line no-control-regex + const controlChars = /[\u0000-\u001F\u007F-\u009F]/; + if (controlChars.test(str)) return true; + + // 3. Optionally, check if most chars are non-printable + const printableRatio = + [...str].filter((c) => c >= ' ' && c <= '~').length / str.length; + if (printableRatio < 0.7) return true; // mostly non-printable → probably binary + + return false; // seems like valid string +}; diff --git a/packages/storage-plugin/src/react-native/storage-view.ts b/packages/storage-plugin/src/react-native/storage-view.ts new file mode 100644 index 00000000..32fbc6bd --- /dev/null +++ b/packages/storage-plugin/src/react-native/storage-view.ts @@ -0,0 +1,245 @@ +import { + getStorageViewId, + supportsType, + type AsyncStorage, + type StorageAdapter, + type StorageCapabilities, + type StorageEntry, + type StorageEntryType, + type StorageNode, + type StorageSubscription, + type StorageTarget, + type SyncStorage, +} from '../shared/types'; + +const POLLING_INTERVAL_MS = 1500; + +type StorageSnapshotMap = Map; + +type AsyncStorageLike = SyncStorage | AsyncStorage; + +const isAsyncStorage = (storage: AsyncStorageLike): storage is AsyncStorage => + storage.kind === 'async'; + +const fingerprintEntry = (entry: StorageEntry) => { + if (entry.type === 'buffer') { + return `${entry.type}:${entry.value.join(',')}`; + } + + return `${entry.type}:${String(entry.value)}`; +}; + +const toSnapshotMap = (entries: StorageEntry[]) => { + return new Map(entries.map((entry) => [entry.key, entry])); +}; + +const shouldFilterKey = (storage: StorageNode, key: string) => { + if (!storage.blacklist) { + return false; + } + + storage.blacklist.lastIndex = 0; + return storage.blacklist.test(key); +}; + +const checkTypeSupport = ( + capabilities: StorageCapabilities, + type: StorageEntryType, + target: StorageTarget +) => { + if (supportsType(capabilities, type)) { + return; + } + + throw new Error( + `Type "${type}" is not supported by storage "${target.storageId}" in adapter "${target.adapterId}".` + ); +}; + +const getAllKeys = async (storage: AsyncStorageLike) => { + if (isAsyncStorage(storage)) { + return storage.getAllKeys(); + } + + return storage.getAllKeys(); +}; + +const getEntry = async (storage: AsyncStorageLike, key: string) => { + if (isAsyncStorage(storage)) { + return storage.get(key); + } + + return storage.get(key); +}; + +const setEntry = async (storage: AsyncStorageLike, entry: StorageEntry) => { + if (isAsyncStorage(storage)) { + await storage.set(entry); + return; + } + + storage.set(entry); +}; + +const deleteEntry = async (storage: AsyncStorageLike, key: string) => { + if (isAsyncStorage(storage)) { + await storage.delete(key); + return; + } + + storage.delete(key); +}; + +export type StorageView = { + id: string; + target: StorageTarget; + adapterName: string; + storageName: string; + capabilities: StorageCapabilities; + get: (key: string) => Promise; + set: (entry: StorageEntry) => Promise; + delete: (key: string) => Promise; + getAllKeys: () => Promise; + getAllEntries: () => Promise; + watch: (callbacks: { + onSet: (entry: StorageEntry) => void; + onDelete: (key: string) => void; + }) => Promise; +}; + +const buildSnapshotMap = async ( + getAllEntries: () => Promise +): Promise => { + const entries = await getAllEntries(); + return toSnapshotMap(entries); +}; + +const diffSnapshots = ( + previous: StorageSnapshotMap, + next: StorageSnapshotMap, + handlers: { + onSet: (entry: StorageEntry) => void; + onDelete: (key: string) => void; + } +) => { + next.forEach((nextEntry, key) => { + const previousEntry = previous.get(key); + + if (!previousEntry) { + handlers.onSet(nextEntry); + return; + } + + if (fingerprintEntry(previousEntry) !== fingerprintEntry(nextEntry)) { + handlers.onSet(nextEntry); + } + }); + + previous.forEach((_value, key) => { + if (!next.has(key)) { + handlers.onDelete(key); + } + }); +}; + +const createPollingSubscription = async ( + getAllEntries: () => Promise, + handlers: { + onSet: (entry: StorageEntry) => void; + onDelete: (key: string) => void; + } +): Promise => { + let previousSnapshot = await buildSnapshotMap(getAllEntries); + + const interval = setInterval(async () => { + try { + const nextSnapshot = await buildSnapshotMap(getAllEntries); + diffSnapshots(previousSnapshot, nextSnapshot, handlers); + previousSnapshot = nextSnapshot; + } catch { + // Silently ignore polling errors and try again on next tick. + } + }, POLLING_INTERVAL_MS); + + return { + remove: () => { + clearInterval(interval); + }, + }; +}; + +export const createStorageView = ( + adapter: StorageAdapter, + storageNode: StorageNode +): StorageView => { + const storage = storageNode.storage; + const target: StorageTarget = { + adapterId: adapter.id, + storageId: storageNode.id, + }; + + const get = async (key: string) => { + if (shouldFilterKey(storageNode, key)) { + return undefined; + } + + return getEntry(storage, key); + }; + + const getAllEntries = async () => { + const keys = await getAllKeys(storage); + const visibleEntries = await Promise.all( + keys + .filter((key) => !shouldFilterKey(storageNode, key)) + .map((key) => getEntry(storage, key)) + ); + + return visibleEntries.filter((entry): entry is StorageEntry => !!entry); + }; + + return { + id: getStorageViewId(target), + target, + adapterName: adapter.name, + storageName: storageNode.name, + capabilities: storageNode.capabilities, + get, + set: async (entry) => { + checkTypeSupport(storageNode.capabilities, entry.type, target); + await setEntry(storage, entry); + }, + delete: async (key) => { + await deleteEntry(storage, key); + }, + getAllKeys: async () => { + const keys = await getAllKeys(storage); + return keys.filter((key) => !shouldFilterKey(storageNode, key)); + }, + getAllEntries, + watch: async ({ onSet, onDelete }) => { + if (storage.subscribe) { + return storage.subscribe(async (key) => { + try { + const entry = await get(key); + + if (!entry) { + onDelete(key); + return; + } + + onSet(entry); + } catch { + // Ignore runtime callback errors; polling fallback is not needed when subscribe exists. + } + }); + } + + return createPollingSubscription(getAllEntries, { onSet, onDelete }); + }, + }; +}; + +export const createStorageViews = (storages: StorageAdapter[]) => + storages.flatMap((adapter) => + adapter.storages.map((storageNode) => createStorageView(adapter, storageNode)) + ); diff --git a/packages/storage-plugin/src/react-native/useRozeniteStoragePlugin.ts b/packages/storage-plugin/src/react-native/useRozeniteStoragePlugin.ts new file mode 100644 index 00000000..f02423f5 --- /dev/null +++ b/packages/storage-plugin/src/react-native/useRozeniteStoragePlugin.ts @@ -0,0 +1,162 @@ +import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge'; +import { useEffect, useMemo } from 'react'; +import type { + StorageDeleteEntryEvent, + StorageEventMap, + StorageGetSnapshotEvent, + StorageSetEntryEvent, +} from '../shared/messaging'; +import type { StorageAdapter } from '../shared/types'; +import { createStorageViews } from './storage-view'; + +export type RozeniteStoragePluginOptions = { + storages: StorageAdapter[]; +}; + +export const useRozeniteStoragePlugin = ({ + storages, +}: RozeniteStoragePluginOptions) => { + const views = useMemo(() => createStorageViews(storages), [storages]); + + const client = useRozeniteDevToolsClient({ + pluginId: '@rozenite/storage-plugin', + }); + + useEffect(() => { + if (!client) { + return; + } + + const pushSnapshot = async (viewId?: string) => { + const selectedViews = viewId ? views.filter((view) => view.id === viewId) : views; + + for (const view of selectedViews) { + try { + const entries = await view.getAllEntries(); + client.send('snapshot', { + type: 'snapshot', + target: view.target, + adapterName: view.adapterName, + storageName: view.storageName, + capabilities: view.capabilities, + entries, + }); + } catch (error) { + console.warn( + `[Rozenite] Storage Plugin: Failed to snapshot ${view.target.adapterId}/${view.target.storageId}.`, + error + ); + } + } + }; + + void pushSnapshot(); + + const viewSubscriptions: { remove: () => void }[] = []; + let disposed = false; + + // Prevent one storage watcher failure from breaking the whole plugin. + void Promise.all( + views.map(async (view) => { + try { + const subscription = await view.watch({ + onSet: (entry) => { + client.send('set-entry', { + type: 'set-entry', + target: view.target, + entry, + }); + }, + onDelete: (key) => { + client.send('delete-entry', { + type: 'delete-entry', + target: view.target, + key, + }); + }, + }); + + if (disposed) { + subscription.remove(); + return; + } + + viewSubscriptions.push(subscription); + } catch (error) { + console.warn( + `[Rozenite] Storage Plugin: Failed to attach watcher for ${view.target.adapterId}/${view.target.storageId}.`, + error + ); + } + }) + ); + + const messageSubscriptions = [ + client.onMessage('set-entry', async ({ target, entry }: StorageSetEntryEvent) => { + const view = views.find( + (candidate) => + candidate.target.adapterId === target.adapterId && + candidate.target.storageId === target.storageId + ); + + if (!view) { + console.warn( + `[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}` + ); + return; + } + + try { + await view.set(entry); + } catch (error) { + console.warn( + `[Rozenite] Storage Plugin: Failed to set entry in ${target.adapterId}/${target.storageId}.`, + error + ); + } + }), + client.onMessage( + 'delete-entry', + async ({ target, key }: StorageDeleteEntryEvent) => { + const view = views.find( + (candidate) => + candidate.target.adapterId === target.adapterId && + candidate.target.storageId === target.storageId + ); + + if (!view) { + console.warn( + `[Rozenite] Storage Plugin: Storage target not found for ${target.adapterId}/${target.storageId}` + ); + return; + } + + try { + await view.delete(key); + } catch (error) { + console.warn( + `[Rozenite] Storage Plugin: Failed to delete entry in ${target.adapterId}/${target.storageId}.`, + error + ); + } + } + ), + client.onMessage('get-snapshot', async ({ target }: StorageGetSnapshotEvent) => { + if (target === 'all') { + await pushSnapshot(); + return; + } + + await pushSnapshot(`${target.adapterId}:${target.storageId}`); + }), + ]; + + return () => { + disposed = true; + viewSubscriptions.forEach((subscription) => subscription.remove()); + messageSubscriptions.forEach((subscription) => subscription.remove()); + }; + }, [client, views]); + + return client; +}; diff --git a/packages/storage-plugin/src/shared/messaging.ts b/packages/storage-plugin/src/shared/messaging.ts new file mode 100644 index 00000000..e5915cd6 --- /dev/null +++ b/packages/storage-plugin/src/shared/messaging.ts @@ -0,0 +1,37 @@ +import type { StorageCapabilities, StorageEntry, StorageTarget } from './types'; + +export type StorageSnapshotEvent = { + type: 'snapshot'; + target: StorageTarget; + adapterName: string; + storageName: string; + capabilities: StorageCapabilities; + entries: StorageEntry[]; +}; + +export type StorageSetEntryEvent = { + type: 'set-entry'; + target: StorageTarget; + entry: StorageEntry; +}; + +export type StorageDeleteEntryEvent = { + type: 'delete-entry'; + target: StorageTarget; + key: string; +}; + +export type StorageGetSnapshotEvent = { + type: 'get-snapshot'; + target: StorageTarget | 'all'; +}; + +export type StorageEvent = + | StorageSnapshotEvent + | StorageSetEntryEvent + | StorageDeleteEntryEvent + | StorageGetSnapshotEvent; + +export type StorageEventMap = { + [K in StorageEvent['type']]: Extract; +}; diff --git a/packages/storage-plugin/src/shared/types.ts b/packages/storage-plugin/src/shared/types.ts new file mode 100644 index 00000000..62f473fb --- /dev/null +++ b/packages/storage-plugin/src/shared/types.ts @@ -0,0 +1,66 @@ +export type StorageEntry = + | { key: string; type: 'string'; value: string } + | { key: string; type: 'number'; value: number } + | { key: string; type: 'boolean'; value: boolean } + | { key: string; type: 'buffer'; value: number[] }; + +export type StorageEntryType = StorageEntry['type']; +export type StorageEntryValue = StorageEntry['value']; + +export type StorageCapabilities = { + supportedTypes: StorageEntryType[]; +}; + +export type StorageSubscription = { remove: () => void }; + +export type SyncStorage = { + kind: 'sync'; + getAllKeys: () => string[]; + get: (key: string) => StorageEntry | undefined; + set: (entry: StorageEntry) => void; + delete: (key: string) => void; + subscribe?: (callback: (key: string) => void) => StorageSubscription; +}; + +export type AsyncStorage = { + kind: 'async'; + getAllKeys: () => Promise; + get: (key: string) => Promise; + set: (entry: StorageEntry) => Promise; + delete: (key: string) => Promise; + subscribe?: (callback: (key: string) => void) => StorageSubscription; +}; + +export type StorageNode = { + id: string; + name: string; + storage: SyncStorage | AsyncStorage; + capabilities: StorageCapabilities; + blacklist?: RegExp; +}; + +export type StorageAdapter = { + id: string; + name: string; + storages: StorageNode[]; +}; + +export type StorageTarget = { + adapterId: string; + storageId: string; +}; + +export const DEFAULT_SUPPORTED_TYPES: StorageEntryType[] = [ + 'string', + 'number', + 'boolean', + 'buffer', +]; + +export const getStorageViewId = ({ adapterId, storageId }: StorageTarget) => + `${adapterId}:${storageId}`; + +export const supportsType = ( + capabilities: StorageCapabilities, + type: StorageEntryType +) => capabilities.supportedTypes.includes(type); diff --git a/packages/storage-plugin/src/ui/add-entry-dialog.tsx b/packages/storage-plugin/src/ui/add-entry-dialog.tsx new file mode 100644 index 00000000..d47e3863 --- /dev/null +++ b/packages/storage-plugin/src/ui/add-entry-dialog.tsx @@ -0,0 +1,310 @@ +import { useMemo, useState } from 'react'; +import { X } from 'lucide-react'; +import type { + StorageEntry, + StorageEntryType, + StorageEntryValue, +} from '../shared/types'; +import { ConfirmDialog } from './confirm-dialog'; + +const TYPE_OPTIONS: Array<{ value: StorageEntryType; label: string }> = [ + { value: 'string', label: 'String' }, + { value: 'number', label: 'Number' }, + { value: 'boolean', label: 'Boolean' }, + { value: 'buffer', label: 'Buffer (Array)' }, +]; + +export type AddEntryDialogProps = { + isOpen: boolean; + onClose: () => void; + onAddEntry: (entry: StorageEntry) => void; + existingKeys: string[]; + supportedTypes: StorageEntryType[]; +}; + +export const AddEntryDialog = ({ + isOpen, + onClose, + onAddEntry, + existingKeys, + supportedTypes, +}: AddEntryDialogProps) => { + const [newEntryKey, setNewEntryKey] = useState(''); + const [newEntryType, setNewEntryType] = useState('string'); + const [newEntryValue, setNewEntryValue] = useState(''); + const [confirmDialog, setConfirmDialog] = useState<{ + isOpen: boolean; + title: string; + message: string; + type: 'confirm' | 'alert'; + onConfirm?: () => void; + }>({ isOpen: false, title: '', message: '', type: 'alert' }); + + const enabledTypes = useMemo( + () => TYPE_OPTIONS.filter((option) => supportedTypes.includes(option.value)), + [supportedTypes] + ); + + const firstSupportedType = enabledTypes[0]?.value; + + const selectedTypeSupported = supportedTypes.includes(newEntryType); + + const resetForm = () => { + setNewEntryKey(''); + setNewEntryType(firstSupportedType ?? 'string'); + setNewEntryValue(''); + onClose(); + }; + + const handleAddEntry = () => { + if (!newEntryKey.trim()) return; + + if (!selectedTypeSupported) { + setConfirmDialog({ + isOpen: true, + title: 'Unsupported Type', + message: 'Selected type is not supported by this storage.', + type: 'alert', + }); + return; + } + + if (existingKeys.includes(newEntryKey)) { + setConfirmDialog({ + isOpen: true, + title: 'Key Already Exists', + message: 'An entry with this key already exists.', + type: 'alert', + }); + return; + } + + let parsedValue: StorageEntryValue; + try { + switch (newEntryType) { + case 'string': + parsedValue = newEntryValue; + break; + case 'number': + parsedValue = Number(newEntryValue); + if (Number.isNaN(parsedValue)) { + throw new Error('Invalid number'); + } + break; + case 'boolean': + if (newEntryValue !== 'true' && newEntryValue !== 'false') { + throw new Error('Boolean value must be true or false'); + } + parsedValue = newEntryValue === 'true'; + break; + case 'buffer': + parsedValue = JSON.parse(newEntryValue); + if ( + !Array.isArray(parsedValue) || + !parsedValue.every((value) => typeof value === 'number') + ) { + throw new Error('Buffer must be an array of numbers'); + } + break; + default: + throw new Error('Invalid type'); + } + } catch (error) { + setConfirmDialog({ + isOpen: true, + title: 'Invalid Value', + message: `Invalid value for ${newEntryType}: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + type: 'alert', + }); + return; + } + + let entry: StorageEntry; + if (newEntryType === 'string') { + entry = { key: newEntryKey, type: 'string', value: parsedValue as string }; + } else if (newEntryType === 'number') { + entry = { key: newEntryKey, type: 'number', value: parsedValue as number }; + } else if (newEntryType === 'boolean') { + entry = { key: newEntryKey, type: 'boolean', value: parsedValue as boolean }; + } else { + entry = { key: newEntryKey, type: 'buffer', value: parsedValue as number[] }; + } + + onAddEntry(entry); + + resetForm(); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + resetForm(); + return; + } + + if (event.key === 'Enter' && newEntryKey.trim() && newEntryValue.trim()) { + handleAddEntry(); + } + }; + + if (!isOpen) { + return null; + } + + return ( +
+
event.stopPropagation()} + onKeyDown={handleKeyDown} + > +
+

Add New Entry

+ +
+ +
+
+ + setNewEntryKey(event.target.value)} + placeholder="Enter key name" + className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500" + autoFocus + /> +
+ +
+ + + {!selectedTypeSupported && ( +

+ Selected type is not supported by this storage. +

+ )} +
+ +
+ + {newEntryType === 'boolean' ? ( + + ) : ( + setNewEntryValue(event.target.value)} + placeholder={ + newEntryType === 'string' + ? 'Enter string value' + : newEntryType === 'number' + ? 'Enter number value' + : newEntryType === 'buffer' + ? 'Enter array as JSON, e.g., [1, 2, 3]' + : 'Enter value' + } + className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + )} + {newEntryType === 'buffer' && ( +

+ Enter as JSON array of numbers, e.g., [1, 2, 3, 255] +

+ )} +
+
+ +
+ + +
+
+ + setConfirmDialog((previous) => ({ ...previous, isOpen: false }))} + onConfirm={() => { + if (confirmDialog.onConfirm) { + confirmDialog.onConfirm(); + } + }} + title={confirmDialog.title} + message={confirmDialog.message} + type={confirmDialog.type} + /> +
+ ); +}; diff --git a/packages/storage-plugin/src/ui/confirm-dialog.tsx b/packages/storage-plugin/src/ui/confirm-dialog.tsx new file mode 100644 index 00000000..fe024a34 --- /dev/null +++ b/packages/storage-plugin/src/ui/confirm-dialog.tsx @@ -0,0 +1,100 @@ +import { X, AlertTriangle, Info } from 'lucide-react'; + +export type ConfirmDialogProps = { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + title: string; + message: string; + type?: 'confirm' | 'alert'; + confirmText?: string; + cancelText?: string; +}; + +export const ConfirmDialog = ({ + isOpen, + onClose, + onConfirm, + title, + message, + type = 'confirm', + confirmText = 'Confirm', + cancelText = 'Cancel', +}: ConfirmDialogProps) => { + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } else if (e.key === 'Enter') { + onConfirm(); + } + }; + + const handleConfirm = () => { + onConfirm(); + onClose(); + }; + + const handleCancel = () => { + onClose(); + }; + + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()} + onKeyDown={handleKeyDown} + > +
+
+ {type === 'confirm' ? ( + + ) : ( + + )} +

{title}

+
+ +
+ +
+

{message}

+
+ + {/* Dialog Actions */} +
+ {type === 'confirm' && ( + + )} + +
+
+
+ ); +}; diff --git a/packages/storage-plugin/src/ui/edit-entry-dialog.tsx b/packages/storage-plugin/src/ui/edit-entry-dialog.tsx new file mode 100644 index 00000000..a0e98e51 --- /dev/null +++ b/packages/storage-plugin/src/ui/edit-entry-dialog.tsx @@ -0,0 +1,280 @@ +import { useEffect, useMemo, useState } from 'react'; +import { X, Edit3 } from 'lucide-react'; +import type { + StorageEntry, + StorageEntryType, + StorageEntryValue, +} from '../shared/types'; +import { ConfirmDialog } from './confirm-dialog'; + +export type EditEntryDialogProps = { + isOpen: boolean; + onClose: () => void; + onEditEntry: (key: string, newValue: StorageEntryValue) => void; + entry: StorageEntry | null; + supportedTypes: StorageEntryType[]; +}; + +export const EditEntryDialog = ({ + isOpen, + onClose, + onEditEntry, + entry, + supportedTypes, +}: EditEntryDialogProps) => { + const [editValue, setEditValue] = useState(''); + const [confirmDialog, setConfirmDialog] = useState<{ + isOpen: boolean; + title: string; + message: string; + type: 'confirm' | 'alert'; + onConfirm?: () => void; + }>({ isOpen: false, title: '', message: '', type: 'alert' }); + + useEffect(() => { + if (entry && isOpen) { + setEditValue(Array.isArray(entry.value) ? JSON.stringify(entry.value) : String(entry.value)); + } + }, [entry, isOpen]); + + const isTypeSupported = useMemo( + () => !!entry && supportedTypes.includes(entry.type), + [entry, supportedTypes] + ); + + const resetForm = () => { + setEditValue(''); + onClose(); + }; + + const handleEditEntry = () => { + if (!entry) return; + + if (!isTypeSupported) { + setConfirmDialog({ + isOpen: true, + title: 'Unsupported Type', + message: 'This storage does not support the current entry type.', + type: 'alert', + }); + return; + } + + let newValue: StorageEntryValue; + + try { + switch (entry.type) { + case 'string': + newValue = editValue; + break; + case 'number': + newValue = Number(editValue); + if (Number.isNaN(newValue)) { + throw new Error('Invalid number'); + } + break; + case 'boolean': + if (editValue !== 'true' && editValue !== 'false') { + throw new Error('Boolean value must be "true" or "false"'); + } + newValue = editValue === 'true'; + break; + case 'buffer': + newValue = JSON.parse(editValue); + if (!Array.isArray(newValue) || !newValue.every((v) => typeof v === 'number')) { + throw new Error('Buffer must be an array of numbers'); + } + break; + default: + throw new Error('Invalid type'); + } + } catch (error) { + setConfirmDialog({ + isOpen: true, + title: 'Invalid Value', + message: `Invalid value for ${entry.type}: ${ + error instanceof Error ? error.message : 'Unknown error' + }`, + type: 'alert', + }); + return; + } + + onEditEntry(entry.key, newValue); + resetForm(); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + resetForm(); + } else if (event.key === 'Enter' && editValue.trim()) { + handleEditEntry(); + } + }; + + const getInputType = (type: StorageEntryType) => { + if (type === 'number') { + return 'number'; + } + + return 'text'; + }; + + const getPlaceholder = (type: StorageEntryType) => { + if (type === 'string') { + return 'Enter string value'; + } + + if (type === 'number') { + return 'Enter number value'; + } + + if (type === 'boolean') { + return 'Enter true or false'; + } + + return 'Enter array as JSON, e.g., [1, 2, 3]'; + }; + + const getTypeColorClass = (type: StorageEntryType) => { + if (type === 'string') { + return 'bg-green-600'; + } + + if (type === 'number') { + return 'bg-blue-600'; + } + + if (type === 'boolean') { + return 'bg-yellow-600'; + } + + return 'bg-purple-600'; + }; + + if (!isOpen || !entry) { + return null; + } + + return ( +
+
event.stopPropagation()} + onKeyDown={handleKeyDown} + > +
+
+ +

Edit Entry

+
+ +
+ +
+
+ +
+ {entry.key} +
+

Key cannot be changed during editing

+
+ +
+ +
+ + {entry.type} + +
+ {!isTypeSupported ? ( +

+ This storage does not support {entry.type} values. +

+ ) : ( +

Type cannot be changed during editing

+ )} +
+ +
+ + {entry.type === 'boolean' ? ( + + ) : ( + setEditValue(event.target.value)} + placeholder={getPlaceholder(entry.type)} + className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500" + autoFocus + /> + )} + {entry.type === 'buffer' && ( +

+ Enter as JSON array of numbers, e.g., [1, 2, 3, 255] +

+ )} +
+
+ +
+ + +
+
+ + setConfirmDialog((previous) => ({ ...previous, isOpen: false }))} + onConfirm={() => { + if (confirmDialog.onConfirm) { + confirmDialog.onConfirm(); + } + }} + title={confirmDialog.title} + message={confirmDialog.message} + type={confirmDialog.type} + /> +
+ ); +}; diff --git a/packages/storage-plugin/src/ui/editable-table.tsx b/packages/storage-plugin/src/ui/editable-table.tsx new file mode 100644 index 00000000..8f1f0d50 --- /dev/null +++ b/packages/storage-plugin/src/ui/editable-table.tsx @@ -0,0 +1,298 @@ +import { useMemo, useState } from 'react'; +import { + createColumnHelper, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + type ColumnDef, + type SortingState, + useReactTable, +} from '@tanstack/react-table'; +import { Edit3, Loader2, Trash2 } from 'lucide-react'; +import type { + StorageEntry, + StorageEntryType, + StorageEntryValue, +} from '../shared/types'; +import { ConfirmDialog } from './confirm-dialog'; +import { EditEntryDialog } from './edit-entry-dialog'; + +export type EditableTableProps = { + data: StorageEntry[]; + supportedTypes: StorageEntryType[]; + onValueChange?: (key: string, newValue: StorageEntryValue) => void; + onDeleteEntry?: (key: string) => void; + onRowClick?: (entry: StorageEntry) => void; + loading?: boolean; +}; + +const columnHelper = createColumnHelper(); + +export const EditableTable = ({ + data, + supportedTypes, + onValueChange, + onDeleteEntry, + onRowClick, + loading = false, +}: EditableTableProps) => { + const [editingEntry, setEditingEntry] = useState(null); + const [showEditDialog, setShowEditDialog] = useState(false); + const [sorting, setSorting] = useState([]); + const [deleteConfirm, setDeleteConfirm] = useState<{ + isOpen: boolean; + entryKey: string; + }>({ isOpen: false, entryKey: '' }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const columns = useMemo[]>( + () => [ + columnHelper.accessor('key', { + header: 'Key', + enableSorting: true, + cell: ({ getValue }) => ( +
{getValue()}
+ ), + }), + columnHelper.accessor('type', { + header: 'Type', + enableSorting: true, + cell: ({ getValue }) => { + const type = getValue() as StorageEntryType; + return ( +
+ + {type} + +
+ ); + }, + }), + columnHelper.accessor('value', { + header: 'Value', + cell: ({ row }) => { + const entry = row.original; + return ( +
+
{formatValue(entry)}
+ +
+ ); + }, + }), + columnHelper.display({ + id: 'actions', + header: 'Actions', + cell: ({ row }) => ( +
+ +
+ ), + }), + ], + [onDeleteEntry] + ); + + const table = useReactTable({ + data, + columns, + state: { sorting }, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + }); + + const handleEdit = (entry: StorageEntry) => { + setEditingEntry(entry); + setShowEditDialog(true); + }; + + const handleEditEntry = (key: string, newValue: StorageEntryValue) => { + if (onValueChange) { + onValueChange(key, newValue); + } + + setEditingEntry(null); + setShowEditDialog(false); + }; + + const handleCloseEditDialog = () => { + setEditingEntry(null); + setShowEditDialog(false); + }; + + const handleDelete = (key: string) => { + if (onDeleteEntry) { + setDeleteConfirm({ isOpen: true, entryKey: key }); + } + }; + + const confirmDelete = () => { + if (onDeleteEntry && deleteConfirm.entryKey) { + onDeleteEntry(deleteConfirm.entryKey); + } + + setDeleteConfirm({ isOpen: false, entryKey: '' }); + }; + + if (loading) { + return ( +
+ +

Loading entries...

+
+ ); + } + + return ( + <> + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + { + const target = event.target as HTMLElement; + if ( + target.tagName === 'BUTTON' || + target.closest('button') || + target.tagName === 'INPUT' || + target.closest('input') + ) { + return; + } + + if (onRowClick) { + onRowClick(row.original); + } + }} + > + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + {header.column.getCanSort() && ( + + {{ + asc: '↑', + desc: '↓', + }[header.column.getIsSorted() as string] ?? '↕'} + + )} +
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ + + + setDeleteConfirm({ isOpen: false, entryKey: '' })} + onConfirm={confirmDelete} + title="Delete Entry" + message={`Are you sure you want to delete the entry "${deleteConfirm.entryKey}"?`} + type="confirm" + confirmText="Delete" + /> + + ); +}; + +const getTypeColorClass = (type: StorageEntryType) => { + if (type === 'string') { + return 'bg-green-600'; + } + + if (type === 'number') { + return 'bg-blue-600'; + } + + if (type === 'boolean') { + return 'bg-yellow-600'; + } + + return 'bg-purple-600'; +}; + +const formatValue = (entry: StorageEntry) => { + if (entry.type === 'string') { + return "{entry.value}"; + } + + if (entry.type === 'number') { + return {entry.value}; + } + + if (entry.type === 'boolean') { + return ( + + {entry.value ? 'true' : 'false'} + + ); + } + + const displayValue = + entry.value.length > 5 + ? `[${entry.value.slice(0, 5).join(', ')}, ...${entry.value.length - 5} more]` + : `[${entry.value.join(', ')}]`; + + return {displayValue}; +}; diff --git a/packages/storage-plugin/src/ui/entry-detail-dialog.tsx b/packages/storage-plugin/src/ui/entry-detail-dialog.tsx new file mode 100644 index 00000000..87a32af3 --- /dev/null +++ b/packages/storage-plugin/src/ui/entry-detail-dialog.tsx @@ -0,0 +1,220 @@ +import { X, Info, Edit3 } from 'lucide-react'; +import { JSONTree } from 'react-json-tree'; +import { StorageEntry } from '../shared/types'; +import { useMemo } from 'react'; + +export type EntryDetailDialogProps = { + isOpen: boolean; + onClose: () => void; + onEdit?: (entry: StorageEntry) => void; + entry: StorageEntry | null; +}; + +const jsonTreeTheme = { + base00: 'transparent', + base01: '#374151', // bg-gray-700 + base02: '#4b5563', // bg-gray-600 + base03: '#6b7280', // text-gray-500 + base04: '#9ca3af', // text-gray-400 + base05: '#d1d5db', // text-gray-300 + base06: '#e5e7eb', // text-gray-200 + base07: '#f9fafb', // text-gray-100 + base08: '#ef4444', // text-red-500 + base09: '#f59e0b', // text-yellow-500 + base0A: '#10b981', // text-green-500 + base0B: '#3b82f6', // text-blue-500 + base0C: '#06b6d4', // text-cyan-500 + base0D: '#8b5cf6', // text-purple-500 + base0E: '#ec4899', // text-pink-500 + base0F: '#f97316', // text-orange-500 +}; + +const jsonSafeParse = (value: string): Record | unknown[] | null => { + try { + const parsed = JSON.parse(value) as unknown; + + if (Array.isArray(parsed)) { + return parsed; + } + + if (parsed && typeof parsed === 'object') { + return parsed as Record; + } + + return null; + } catch { + return null; + } +}; + +const getTypeColorClass = (type: string) => { + switch (type) { + case 'string': + return 'bg-green-600'; + case 'number': + return 'bg-blue-600'; + case 'boolean': + return 'bg-yellow-600'; + case 'buffer': + return 'bg-purple-600'; + default: + return 'bg-gray-600'; + } +}; + +const formatValue = (entry: StorageEntry) => { + switch (entry.type) { + case 'string': + return ( + + "{entry.value as string}" + + ); + case 'number': + return ( + {entry.value as number} + ); + case 'boolean': + return ( + + {entry.value ? 'true' : 'false'} + + ); + case 'buffer': { + const bufferArray = entry.value as number[]; + return ( + + [{bufferArray.join(', ')}] + + ); + } + default: + return Unknown; + } +}; + +export const EntryDetailDialog = ({ + isOpen, + onClose, + onEdit, + entry, +}: EntryDetailDialogProps) => { + const isStringValue = entry?.type === 'string'; + const stringValue = isStringValue ? (entry.value as string) : ''; + const jsonValue = useMemo( + () => (isStringValue ? jsonSafeParse(stringValue) : null), + [isStringValue, stringValue] + ); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } + }; + + if (!isOpen || !entry) return null; + + return ( +
+
e.stopPropagation()} + onKeyDown={handleKeyDown} + > +
+
+ +

+ Entry Details +

+
+ +
+ +
+ {/* Key Display */} +
+ +
+ {entry.key} +
+
+ + {/* Type Display */} +
+ +
+ + {entry.type} + +
+
+ + {/* Value Display */} +
+ +
+ {jsonValue ? ( + keyPath.length <= 2} + /> + ) : ( +
{formatValue(entry)}
+ )} +
+
+
+ + {/* Dialog Actions */} +
+ {onEdit && ( + + )} + +
+
+
+ ); +}; diff --git a/packages/storage-plugin/src/ui/globals.css b/packages/storage-plugin/src/ui/globals.css new file mode 100644 index 00000000..2bf47da0 --- /dev/null +++ b/packages/storage-plugin/src/ui/globals.css @@ -0,0 +1,123 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer utilities { + .text-balance { + text-wrap: balance; + } + + .wrap-anywhere { + overflow-wrap: anywhere; + } +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + +/* Custom scrollbar styles to match plugin theme */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #1f2937; /* gray-800 */ + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: #374151; /* gray-700 */ + border-radius: 4px; + border: 1px solid #1f2937; /* gray-800 */ +} + +::-webkit-scrollbar-thumb:hover { + background: #4b5563; /* gray-600 */ +} + +::-webkit-scrollbar-thumb:active { + background: #6b7280; /* gray-500 */ +} + +::-webkit-scrollbar-corner { + background: #1f2937; /* gray-800 */ +} diff --git a/packages/storage-plugin/src/ui/panel.tsx b/packages/storage-plugin/src/ui/panel.tsx new file mode 100644 index 00000000..9425006b --- /dev/null +++ b/packages/storage-plugin/src/ui/panel.tsx @@ -0,0 +1,442 @@ +import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge'; +import { useEffect, useMemo, useState } from 'react'; +import { Search, Plus } from 'lucide-react'; +import type { + StorageDeleteEntryEvent, + StorageEventMap, + StorageSetEntryEvent, + StorageSnapshotEvent, +} from '../shared/messaging'; +import type { + StorageCapabilities, + StorageEntry, + StorageEntryValue, + StorageTarget, +} from '../shared/types'; +import { getStorageViewId } from '../shared/types'; +import { EditableTable } from './editable-table'; +import { AddEntryDialog } from './add-entry-dialog'; +import { EntryDetailDialog } from './entry-detail-dialog'; +import { EditEntryDialog } from './edit-entry-dialog'; +import './globals.css'; + +type StorageSnapshotState = { + target: StorageTarget; + adapterName: string; + storageName: string; + capabilities: StorageCapabilities; + entries: StorageEntry[]; +}; + +const getEntryTypeFromValue = (value: StorageEntryValue): StorageEntry['type'] => { + if (typeof value === 'string') { + return 'string'; + } + + if (typeof value === 'number') { + return 'number'; + } + + if (typeof value === 'boolean') { + return 'boolean'; + } + + return 'buffer'; +}; + +export default function StoragePanel() { + const [snapshots, setSnapshots] = useState>( + new Map() + ); + const [selectedStorageViewId, setSelectedStorageViewId] = useState( + null + ); + const [loading, setLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [showAddDialog, setShowAddDialog] = useState(false); + const [selectedEntry, setSelectedEntry] = useState(null); + const [showDetailDialog, setShowDetailDialog] = useState(false); + const [editingEntry, setEditingEntry] = useState(null); + const [showEditDialog, setShowEditDialog] = useState(false); + + const client = useRozeniteDevToolsClient({ + pluginId: '@rozenite/storage-plugin', + }); + + useEffect(() => { + if (!client) { + return; + } + + const snapshotSubscription = client.onMessage( + 'snapshot', + (event: StorageSnapshotEvent) => { + const viewId = getStorageViewId(event.target); + setSnapshots((previous) => { + const next = new Map(previous); + next.set(viewId, { + target: event.target, + adapterName: event.adapterName, + storageName: event.storageName, + capabilities: event.capabilities, + entries: event.entries, + }); + + if (previous.size === 0 && !selectedStorageViewId) { + setSelectedStorageViewId(viewId); + } + + return next; + }); + + if (viewId === selectedStorageViewId) { + setLoading(false); + } + } + ); + + const setEntrySubscription = client.onMessage( + 'set-entry', + (event: StorageSetEntryEvent) => { + const viewId = getStorageViewId(event.target); + setSnapshots((previous) => { + const next = new Map(previous); + const current = next.get(viewId); + + if (!current) { + return previous; + } + + const existingIndex = current.entries.findIndex( + (entry) => entry.key === event.entry.key + ); + + const entries = + existingIndex >= 0 + ? current.entries.map((entry) => + entry.key === event.entry.key ? event.entry : entry + ) + : [...current.entries, event.entry]; + + next.set(viewId, { + ...current, + entries, + }); + + return next; + }); + } + ); + + const deleteEntrySubscription = client.onMessage( + 'delete-entry', + (event: StorageDeleteEntryEvent) => { + const viewId = getStorageViewId(event.target); + + setSnapshots((previous) => { + const next = new Map(previous); + const current = next.get(viewId); + + if (!current) { + return previous; + } + + next.set(viewId, { + ...current, + entries: current.entries.filter((entry) => entry.key !== event.key), + }); + + return next; + }); + } + ); + + client.send('get-snapshot', { + type: 'get-snapshot', + target: 'all', + }); + + return () => { + snapshotSubscription.remove(); + setEntrySubscription.remove(); + deleteEntrySubscription.remove(); + }; + }, [client, selectedStorageViewId]); + + useEffect(() => { + if (!client || !selectedStorageViewId) { + return; + } + + const selectedSnapshot = snapshots.get(selectedStorageViewId); + + if (selectedSnapshot) { + setLoading(false); + return; + } + + const separatorIndex = selectedStorageViewId.indexOf(':'); + if (separatorIndex < 0) { + console.warn( + `[Rozenite] Storage Plugin: Invalid storage view id "${selectedStorageViewId}".` + ); + setLoading(false); + return; + } + + const adapterId = selectedStorageViewId.slice(0, separatorIndex); + const storageId = selectedStorageViewId.slice(separatorIndex + 1); + + setLoading(true); + client.send('get-snapshot', { + type: 'get-snapshot', + target: { + adapterId, + storageId, + }, + }); + }, [client, selectedStorageViewId, snapshots]); + + const selectedStorage = selectedStorageViewId + ? snapshots.get(selectedStorageViewId) ?? null + : null; + + const entries = selectedStorage?.entries ?? []; + + const filteredEntries = useMemo( + () => + entries.filter((entry) => + entry.key.toLowerCase().includes(searchTerm.toLowerCase()) + ), + [entries, searchTerm] + ); + + const supportedTypes = selectedStorage?.capabilities.supportedTypes ?? []; + + const updateEntriesForSelectedStorage = ( + mutate: (entries: StorageEntry[]) => StorageEntry[] + ) => { + if (!selectedStorageViewId) { + return; + } + + setSnapshots((previous) => { + const next = new Map(previous); + const current = next.get(selectedStorageViewId); + + if (!current) { + return previous; + } + + next.set(selectedStorageViewId, { + ...current, + entries: mutate(current.entries), + }); + + return next; + }); + }; + + const handleValueChange = (key: string, newValue: StorageEntryValue) => { + if (!client || !selectedStorage) { + return; + } + + const type = getEntryTypeFromValue(newValue); + + if (!selectedStorage.capabilities.supportedTypes.includes(type)) { + return; + } + + let updatedEntry: StorageEntry; + if (type === 'string') { + updatedEntry = { key, type: 'string', value: newValue as string }; + } else if (type === 'number') { + updatedEntry = { key, type: 'number', value: newValue as number }; + } else if (type === 'boolean') { + updatedEntry = { key, type: 'boolean', value: newValue as boolean }; + } else { + updatedEntry = { key, type: 'buffer', value: newValue as number[] }; + } + + client.send('set-entry', { + type: 'set-entry', + target: selectedStorage.target, + entry: updatedEntry, + }); + + updateEntriesForSelectedStorage((currentEntries) => + currentEntries.map((entry) => (entry.key === key ? updatedEntry : entry)) + ); + }; + + const handleDeleteEntry = (key: string) => { + if (!client || !selectedStorage) { + return; + } + + client.send('delete-entry', { + type: 'delete-entry', + target: selectedStorage.target, + key, + }); + + updateEntriesForSelectedStorage((currentEntries) => + currentEntries.filter((entry) => entry.key !== key) + ); + }; + + const handleAddEntry = (entry: StorageEntry) => { + if (!client || !selectedStorage) { + return; + } + + client.send('set-entry', { + type: 'set-entry', + target: selectedStorage.target, + entry, + }); + + updateEntriesForSelectedStorage((currentEntries) => [...currentEntries, entry]); + }; + + const storageOptions = [...snapshots.entries()].map(([viewId, snapshot]) => ({ + viewId, + label: `${snapshot.adapterName} / ${snapshot.storageName}`, + })); + + return ( +
+
+
+ Storage +
+
+
+ + +
+
+ +
+ +
+
+ + setSearchTerm(event.target.value)} + className="h-8 w-full pl-8 pr-3 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+
+ {filteredEntries.length} of {entries.length} entries +
+
+ +
+ {selectedStorage ? ( + filteredEntries.length === 0 ? ( +
+

+ No entries found +

+

+ {searchTerm + ? 'Try adjusting your search terms' + : 'This storage appears to be empty'} +

+
+ ) : ( + { + setSelectedEntry(entry); + setShowDetailDialog(true); + }} + loading={loading} + /> + ) + ) : ( +
+

+ Welcome to Storage Inspector +

+

+ Select a storage from the dropdown above to inspect data +

+
+ )} +
+ + setShowAddDialog(false)} + onAddEntry={handleAddEntry} + existingKeys={entries.map((entry) => entry.key)} + supportedTypes={supportedTypes} + /> + + { + setShowDetailDialog(false); + setSelectedEntry(null); + }} + onEdit={(entry) => { + setShowDetailDialog(false); + setEditingEntry(entry); + setShowEditDialog(true); + }} + entry={selectedEntry} + /> + + { + setShowEditDialog(false); + setEditingEntry(null); + }} + onEditEntry={(key, newValue) => { + handleValueChange(key, newValue); + setShowEditDialog(false); + setEditingEntry(null); + }} + supportedTypes={supportedTypes} + entry={editingEntry} + /> +
+ ); +} diff --git a/packages/storage-plugin/tailwind.config.ts b/packages/storage-plugin/tailwind.config.ts new file mode 100644 index 00000000..d756b75a --- /dev/null +++ b/packages/storage-plugin/tailwind.config.ts @@ -0,0 +1,94 @@ +import type { Config } from 'tailwindcss'; + +const config: Config = { + darkMode: ['class'], + content: ['./src/ui/**/*.{js,ts,jsx,tsx,mdx}'], + theme: { + extend: { + translate: { + '0.75': '0.1875rem', + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))', + }, + sidebar: { + DEFAULT: 'hsl(var(--sidebar-background))', + foreground: 'hsl(var(--sidebar-foreground))', + primary: 'hsl(var(--sidebar-primary))', + 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', + accent: 'hsl(var(--sidebar-accent))', + 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', + border: 'hsl(var(--sidebar-border))', + ring: 'hsl(var(--sidebar-ring))', + }, + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + keyframes: { + 'accordion-down': { + from: { + height: '0', + }, + to: { + height: 'var(--radix-accordion-content-height)', + }, + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)', + }, + to: { + height: '0', + }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + }, + }, + }, + plugins: [require('tailwindcss-animate')], +}; +export default config; diff --git a/packages/storage-plugin/tsconfig.json b/packages/storage-plugin/tsconfig.json new file mode 100644 index 00000000..c1d4d562 --- /dev/null +++ b/packages/storage-plugin/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "ESNext", + "moduleResolution": "bundler", + "baseUrl": ".", + "paths": { + "@rozenite/plugin-bridge": ["../plugin-bridge/src/index.ts"] + }, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src/**/*", "react-native.ts", "rozenite.config.ts"], + "exclude": ["node_modules", "dist", "build"], + "references": [ + { + "path": "../plugin-bridge" + }, + { + "path": "../cli" + }, + { + "path": "../vite-plugin" + } + ] +} diff --git a/packages/storage-plugin/vite.config.ts b/packages/storage-plugin/vite.config.ts new file mode 100644 index 00000000..7db548c7 --- /dev/null +++ b/packages/storage-plugin/vite.config.ts @@ -0,0 +1,20 @@ +/// +import { defineConfig } from 'vite'; +import { rozenitePlugin } from '@rozenite/vite-plugin'; + +export default defineConfig({ + root: __dirname, + plugins: [rozenitePlugin()], + base: './', + build: { + outDir: './dist', + emptyOutDir: false, + reportCompressedSize: false, + minify: true, + sourcemap: false, + }, + server: { + port: 3000, + open: true, + }, +}); diff --git a/plugin-directory.json b/plugin-directory.json index 5e7073ff..0f4b8291 100644 --- a/plugin-directory.json +++ b/plugin-directory.json @@ -27,6 +27,10 @@ "npmUrl": "https://www.npmjs.com/package/@rozenite/mmkv-plugin", "githubUrl": "https://github.com/callstackincubator/rozenite/tree/main/packages/mmkv-plugin" }, + { + "npmUrl": "https://www.npmjs.com/package/@rozenite/storage-plugin", + "githubUrl": "https://github.com/callstackincubator/rozenite/tree/main/packages/storage-plugin" + }, { "npmUrl": "https://www.npmjs.com/package/@rozenite/require-profiler-plugin", "githubUrl": "https://github.com/callstackincubator/rozenite/tree/main/packages/require-profiler-plugin" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 064876cf..8a88c687 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,7 +118,10 @@ importers: dependencies: '@expo/vector-icons': specifier: ^15.0.3 - version: 15.0.3(expo-font@55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + version: 15.0.3(expo-font@55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@react-native-async-storage/async-storage': + specifier: ^3.0.1 + version: 3.0.1(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) '@react-navigation/bottom-tabs': specifier: ^7.4.7 version: 7.4.7(@react-navigation/native@7.1.28(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@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.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-screens@4.22.0(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) @@ -161,6 +164,9 @@ importers: '@rozenite/require-profiler-plugin': specifier: workspace:* version: link:../../packages/require-profiler-plugin + '@rozenite/storage-plugin': + specifier: workspace:* + version: link:../../packages/storage-plugin '@rozenite/tanstack-query-plugin': specifier: workspace:* version: link:../../packages/tanstack-query-plugin @@ -175,37 +181,40 @@ importers: version: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) expo-constants: specifier: ~55.0.4 - version: 55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) + version: 55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) expo-font: specifier: ~55.0.3 - version: 55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + version: 55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) expo-haptics: specifier: ~55.0.5 - version: 55.0.5(expo@55.0.0-preview.10) + version: 55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)) expo-image: specifier: ~55.0.3 - version: 55.0.3(expo@55.0.0-preview.10)(react-native-web@0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + version: 55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@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))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) expo-linking: specifier: ~55.0.4 version: 55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) expo-router: specifier: ~55.0.0-preview.7 - version: 55.0.0-preview.7(cf9b05b365231229bf99876516ee7470) + version: 55.0.0-preview.7(mfjfgdrtx2hkscbkfatqrdcoli) + expo-secure-store: + specifier: ^55.0.8 + version: 55.0.8(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)) expo-splash-screen: specifier: ~55.0.5 - version: 55.0.5(expo@55.0.0-preview.10) + version: 55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)) expo-status-bar: specifier: ~55.0.2 version: 55.0.2(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) expo-symbols: specifier: ~55.0.3 - version: 55.0.3(expo-font@55.0.3)(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + version: 55.0.3(expo-font@55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) expo-system-ui: specifier: ~55.0.5 - version: 55.0.5(expo@55.0.0-preview.10)(react-native-web@0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) + version: 55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@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))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) expo-web-browser: specifier: ~55.0.5 - version: 55.0.5(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) + version: 55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) react: specifier: 'catalog:' version: 19.2.0 @@ -364,7 +373,7 @@ importers: version: 3.7.0 expo-atlas: specifier: ^0.4.0 - version: 0.4.0(expo@55.0.0-preview.10) + version: 0.4.0(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)) devDependencies: '@rozenite/tools': specifier: workspace:* @@ -995,6 +1004,73 @@ importers: specifier: ^2.3.0 version: 2.8.1 + packages/storage-plugin: + dependencies: + '@rozenite/plugin-bridge': + specifier: workspace:* + version: link:../plugin-bridge + devDependencies: + '@rozenite/vite-plugin': + specifier: workspace:* + version: link:../vite-plugin + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@types/react': + specifier: 'catalog:' + version: 19.2.14 + autoprefixer: + specifier: ^10.4.21 + version: 10.4.21(postcss@8.5.6) + lucide-react: + specifier: ^0.263.1 + version: 0.263.1(react@19.2.0) + postcss: + specifier: ^8.5.6 + version: 8.5.6 + react: + specifier: 'catalog:' + version: 19.2.0 + react-dom: + specifier: 'catalog:' + version: 19.2.0(react@19.2.0) + react-json-tree: + specifier: ^0.20.0 + version: 0.20.0(@types/react@19.2.14)(react@19.2.0) + react-native: + specifier: 'catalog:' + version: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) + react-native-mmkv: + specifier: ^3.3.0 + version: 3.3.0(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + react-native-mmkv-v3: + specifier: npm:react-native-mmkv@^3.0.0 + version: react-native-mmkv@3.3.0(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + react-native-mmkv-v4: + specifier: npm:react-native-mmkv@^4.0.0 + version: react-native-mmkv@4.0.0(react-native-nitro-modules@0.31.4(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + react-native-nitro-modules: + specifier: '*' + version: 0.31.4(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + react-native-web: + specifier: ^0.21.2 + version: 0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + rozenite: + specifier: workspace:* + version: link:../cli + tailwindcss: + specifier: ^3.4.17 + version: 3.4.17 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.17) + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: 'catalog:' + version: 7.3.1(@types/node@22.17.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1) + packages/tanstack-query-plugin: dependencies: '@rozenite/plugin-bridge': @@ -1072,7 +1148,7 @@ importers: version: 3.1.10 vite-plugin-dts: specifier: ~4.5.0 - version: 4.5.4(@types/node@22.17.0)(rollup@4.44.1)(typescript@5.8.3)(vite@7.3.1(@types/node@22.17.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1)) + version: 4.5.4(@types/node@22.17.0)(rollup@4.44.1)(typescript@5.9.3)(vite@7.3.1(@types/node@22.17.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1)) vite-plugin-react-native-web: specifier: ^2.1.1 version: 2.1.1 @@ -2359,7 +2435,7 @@ packages: peerDependencies: expo-font: '>=14.0.4' react: '*' - react-native: '*' + react-native: 0.76.0 '@expo/ws-tunnel@1.0.6': resolution: {integrity: sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q==} @@ -3327,6 +3403,12 @@ packages: '@types/react-dom': optional: true + '@react-native-async-storage/async-storage@3.0.1': + resolution: {integrity: sha512-VHwHb19sMg4Xh3W5M6YmJ/HSm1uh8RYFa6Dozm9o/jVYTYUgz2BmDXqXF7sum3glQaR34/hlwVc94px1sSdC2A==} + peerDependencies: + react: '*' + react-native: '*' + '@react-native-harness/babel-preset@1.0.0-alpha.25': resolution: {integrity: sha512-n5nI9iJyXYSai9wbj6x64puN568uKGB4kX/GD+A2No6Me6DcnYz4eMEZPjR5feCCtnfuL0RbvduCzXCUWd6TWw==} peerDependencies: @@ -6381,6 +6463,11 @@ packages: react-server-dom-webpack: optional: true + expo-secure-store@55.0.8: + resolution: {integrity: sha512-8w9tQe8U6oRo5YIzqCqVhRrOnfoODNDoitBtLXEx+zS6WLUnkRq5kH7ViJuOgiM7PzLr9pvAliRiDOKyvFbTuQ==} + peerDependencies: + expo: '*' + expo-server@55.0.3: resolution: {integrity: sha512-DeFRWvLb7pcxqrvDFK95Eujh/VFddRfmTGbcTcPVMhYl6om7eKYe8CgpfqIi2mK0rOnLHw7DPymmcPXXWuFbFw==} engines: {node: '>=20.16.0'} @@ -7021,6 +7108,9 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + idb@8.0.3: + resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -12214,7 +12304,7 @@ snapshots: '@expo-google-fonts/material-symbols@0.4.22': {} - '@expo/cli@55.0.7(@expo/metro-runtime@55.0.5)(expo-constants@55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)))(expo-font@55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(expo@55.0.0-preview.10)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)': + '@expo/cli@55.0.7(bt7yl4nwsruljbnpm4qr6emjei)': dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/config': 55.0.4 @@ -12223,14 +12313,14 @@ snapshots: '@expo/env': 2.1.0 '@expo/image-utils': 0.8.12 '@expo/json-file': 10.0.12 - '@expo/log-box': 55.0.6(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@expo/log-box': 55.0.6(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) '@expo/metro': 54.2.0 - '@expo/metro-config': 55.0.5(expo@55.0.0-preview.10) + '@expo/metro-config': 55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)) '@expo/osascript': 2.4.2 '@expo/package-manager': 1.10.3 '@expo/plist': 0.5.2 - '@expo/prebuild-config': 55.0.4(expo@55.0.0-preview.10) - '@expo/router-server': 55.0.5(@expo/metro-runtime@55.0.5)(expo-constants@55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)))(expo-font@55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(expo-server@55.0.3)(expo@55.0.0-preview.10)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@expo/prebuild-config': 55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)) + '@expo/router-server': 55.0.5(wzc5kkbapyihdjhrbopr2pqcca) '@expo/schema-utils': 55.0.2 '@expo/spawn-async': 1.7.2 '@expo/ws-tunnel': 1.0.6 @@ -12289,7 +12379,7 @@ snapshots: - supports-color - utf-8-validate - '@expo/cli@55.0.7(@expo/metro-runtime@55.0.5)(expo-constants@55.0.4)(expo-font@55.0.3)(expo-router@55.0.0-preview.7)(expo@55.0.0-preview.10)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)': + '@expo/cli@55.0.7(u4h2ohzskzybnnlmfi7ygsjfkm)': dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/config': 55.0.4 @@ -12298,14 +12388,14 @@ snapshots: '@expo/env': 2.1.0 '@expo/image-utils': 0.8.12 '@expo/json-file': 10.0.12 - '@expo/log-box': 55.0.6(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@expo/log-box': 55.0.6(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) '@expo/metro': 54.2.0 - '@expo/metro-config': 55.0.5(expo@55.0.0-preview.10) + '@expo/metro-config': 55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)) '@expo/osascript': 2.4.2 '@expo/package-manager': 1.10.3 '@expo/plist': 0.5.2 - '@expo/prebuild-config': 55.0.4(expo@55.0.0-preview.10) - '@expo/router-server': 55.0.5(@expo/metro-runtime@55.0.5)(expo-constants@55.0.4)(expo-font@55.0.3)(expo-router@55.0.0-preview.7)(expo-server@55.0.3)(expo@55.0.0-preview.10)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@expo/prebuild-config': 55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)) + '@expo/router-server': 55.0.5(n3b4arpwkcfc4zalnbzoxanvwy) '@expo/schema-utils': 55.0.2 '@expo/spawn-async': 1.7.2 '@expo/ws-tunnel': 1.0.6 @@ -12352,7 +12442,7 @@ snapshots: ws: 8.18.3 zod: 3.25.76 optionalDependencies: - expo-router: 55.0.0-preview.7(cf9b05b365231229bf99876516ee7470) + expo-router: 55.0.0-preview.7(mfjfgdrtx2hkscbkfatqrdcoli) react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) transitivePeerDependencies: - '@expo/metro-runtime' @@ -12421,12 +12511,18 @@ snapshots: react: 19.2.0 react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) - '@expo/dom-webview@55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)': + '@expo/dom-webview@55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)': dependencies: expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) react: 19.2.0 react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) + '@expo/dom-webview@55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)': + dependencies: + expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) + '@expo/env@2.1.0': dependencies: chalk: 4.1.2 @@ -12472,16 +12568,25 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/log-box@55.0.6(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)': + '@expo/log-box@55.0.6(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)': dependencies: - '@expo/dom-webview': 55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@expo/dom-webview': 55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) anser: 1.4.10 expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) react: 19.2.0 react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) stacktrace-parser: 0.1.11 - '@expo/metro-config@55.0.5(expo@55.0.0-preview.10)': + '@expo/log-box@55.0.6(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)': + dependencies: + '@expo/dom-webview': 55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + anser: 1.4.10 + expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) + stacktrace-parser: 0.1.11 + + '@expo/metro-config@55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))': dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.28.0 @@ -12509,9 +12614,37 @@ snapshots: - supports-color - utf-8-validate - '@expo/metro-runtime@55.0.5(expo@55.0.0-preview.10)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)': + '@expo/metro-config@55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))': dependencies: - '@expo/log-box': 55.0.6(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@babel/code-frame': 7.27.1 + '@babel/core': 7.28.0 + '@babel/generator': 7.28.3 + '@expo/config': 55.0.4 + '@expo/env': 2.1.0 + '@expo/json-file': 10.0.12 + '@expo/metro': 54.2.0 + '@expo/spawn-async': 1.7.2 + browserslist: 4.26.3 + chalk: 4.1.2 + debug: 4.4.1(supports-color@5.5.0) + getenv: 2.0.0 + glob: 13.0.2 + hermes-parser: 0.29.1 + jsc-safe-url: 0.2.4 + lightningcss: 1.31.1 + minimatch: 9.0.5 + postcss: 8.4.49 + resolve-from: 5.0.0 + optionalDependencies: + expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@expo/metro-runtime@55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)': + dependencies: + '@expo/log-box': 55.0.6(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) anser: 1.4.10 expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) pretty-format: 29.7.0 @@ -12522,6 +12655,20 @@ snapshots: optionalDependencies: react-dom: 19.2.0(react@19.2.0) + '@expo/metro-runtime@55.0.5(expo@55.0.0-preview.10)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)': + dependencies: + '@expo/log-box': 55.0.6(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + anser: 1.4.10 + expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@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.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@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) + optional: true + '@expo/metro@54.2.0': dependencies: metro: 0.83.3 @@ -12562,7 +12709,7 @@ snapshots: base64-js: 1.5.1 xmlbuilder: 15.1.1 - '@expo/prebuild-config@55.0.4(expo@55.0.0-preview.10)': + '@expo/prebuild-config@55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))': dependencies: '@expo/config': 55.0.4 '@expo/config-plugins': 55.0.4 @@ -12578,31 +12725,47 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/router-server@55.0.5(@expo/metro-runtime@55.0.5)(expo-constants@55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)))(expo-font@55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(expo-server@55.0.3)(expo@55.0.0-preview.10)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@expo/prebuild-config@55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))': dependencies: + '@expo/config': 55.0.4 + '@expo/config-plugins': 55.0.4 + '@expo/config-types': 55.0.4 + '@expo/image-utils': 0.8.12 + '@expo/json-file': 10.0.12 + '@react-native/normalize-colors': 0.83.1 debug: 4.4.1(supports-color@5.5.0) expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) - expo-constants: 55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) - expo-font: 55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + resolve-from: 5.0.0 + semver: 7.7.2 + xml2js: 0.6.0 + transitivePeerDependencies: + - supports-color + + '@expo/router-server@55.0.5(n3b4arpwkcfc4zalnbzoxanvwy)': + dependencies: + debug: 4.4.1(supports-color@5.5.0) + expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo-constants: 55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) + expo-font: 55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) expo-server: 55.0.3 react: 19.2.0 optionalDependencies: - '@expo/metro-runtime': 55.0.5(expo@55.0.0-preview.10)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@expo/metro-runtime': 55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo-router: 55.0.0-preview.7(mfjfgdrtx2hkscbkfatqrdcoli) react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: - supports-color - '@expo/router-server@55.0.5(@expo/metro-runtime@55.0.5)(expo-constants@55.0.4)(expo-font@55.0.3)(expo-router@55.0.0-preview.7)(expo-server@55.0.3)(expo@55.0.0-preview.10)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + '@expo/router-server@55.0.5(wzc5kkbapyihdjhrbopr2pqcca)': dependencies: debug: 4.4.1(supports-color@5.5.0) - expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) - expo-constants: 55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) - expo-font: 55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo-constants: 55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) + expo-font: 55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) expo-server: 55.0.3 react: 19.2.0 optionalDependencies: '@expo/metro-runtime': 55.0.5(expo@55.0.0-preview.10)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) - expo-router: 55.0.0-preview.7(cf9b05b365231229bf99876516ee7470) react-dom: 19.2.0(react@19.2.0) transitivePeerDependencies: - supports-color @@ -12626,9 +12789,15 @@ snapshots: '@expo/sudo-prompt@9.3.2': {} - '@expo/vector-icons@15.0.3(expo-font@55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)': + '@expo/vector-icons@15.0.3(expo-font@55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)': + dependencies: + expo-font: 55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) + + '@expo/vector-icons@15.0.3(expo-font@55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)': dependencies: - expo-font: 55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo-font: 55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) react: 19.2.0 react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) @@ -13803,6 +13972,12 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.1.11(@types/react@19.2.14) + '@react-native-async-storage/async-storage@3.0.1(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)': + dependencies: + idb: 8.0.3 + react: 19.2.0 + react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) + '@react-native-harness/babel-preset@1.0.0-alpha.25(@babel/core@7.28.0)(@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.0))': dependencies: '@babel/core': 7.28.0 @@ -15806,6 +15981,19 @@ snapshots: optionalDependencies: typescript: 5.8.3 + '@vue/language-core@2.2.0(typescript@5.9.3)': + dependencies: + '@volar/language-core': 2.4.17 + '@vue/compiler-dom': 3.5.17 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.17 + alien-signals: 0.4.14 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.9.3 + '@vue/shared@3.5.17': {} '@webcomponents/custom-elements@1.6.0': {} @@ -16215,7 +16403,7 @@ snapshots: '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.0) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.0) - babel-preset-expo@55.0.4(@babel/core@7.28.0)(@babel/runtime@7.26.10)(expo@55.0.0-preview.10)(react-refresh@0.14.2): + babel-preset-expo@55.0.4(@babel/core@7.28.0)(@babel/runtime@7.26.10)(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-refresh@0.14.2): dependencies: '@babel/generator': 7.28.3 '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) @@ -16248,6 +16436,39 @@ snapshots: - '@babel/core' - supports-color + babel-preset-expo@55.0.4(@babel/core@7.28.0)(@babel/runtime@7.26.10)(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-refresh@0.14.2): + dependencies: + '@babel/generator': 7.28.3 + '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) + '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.28.0) + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-syntax-export-default-from': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.28.0) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.0) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-runtime': 7.28.0(@babel/core@7.28.0) + '@babel/preset-react': 7.28.5(@babel/core@7.28.0) + '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) + '@react-native/babel-preset': 0.83.1 + babel-plugin-react-compiler: 1.0.0 + babel-plugin-react-native-web: 0.21.2 + babel-plugin-syntax-hermes-parser: 0.29.1 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.28.0) + debug: 4.4.1(supports-color@5.5.0) + react-refresh: 0.14.2 + resolve-from: 5.0.0 + optionalDependencies: + '@babel/runtime': 7.26.10 + expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + babel-preset-jest@29.6.3(@babel/core@7.28.0): dependencies: '@babel/core': 7.28.0 @@ -17392,7 +17613,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: @@ -17423,7 +17644,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.37.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -17651,17 +17872,27 @@ snapshots: expect-type@1.2.2: {} - expo-asset@55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): + expo-asset@55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): dependencies: '@expo/image-utils': 0.8.12 expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) - expo-constants: 55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) + expo-constants: 55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) react: 19.2.0 react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) transitivePeerDependencies: - supports-color - expo-atlas@0.4.0(expo@55.0.0-preview.10): + expo-asset@55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): + dependencies: + '@expo/image-utils': 0.8.12 + expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo-constants: 55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) + react: 19.2.0 + react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) + transitivePeerDependencies: + - supports-color + + expo-atlas@0.4.0(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)): dependencies: '@expo/server': 0.5.3 arg: 5.0.2 @@ -17679,7 +17910,7 @@ snapshots: transitivePeerDependencies: - supports-color - expo-constants@55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)): + expo-constants@55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)): dependencies: '@expo/config': 55.0.4 '@expo/env': 2.1.0 @@ -17688,29 +17919,50 @@ snapshots: transitivePeerDependencies: - supports-color - expo-file-system@55.0.5(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)): + expo-constants@55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)): + dependencies: + '@expo/config': 55.0.4 + '@expo/env': 2.1.0 + expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) + transitivePeerDependencies: + - supports-color + + expo-file-system@55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)): dependencies: expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) - expo-font@55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): + expo-file-system@55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)): + dependencies: + expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) + + expo-font@55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): dependencies: expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) fontfaceobserver: 2.3.0 react: 19.2.0 react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) - expo-glass-effect@55.0.5(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): + expo-font@55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): + dependencies: + expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + fontfaceobserver: 2.3.0 + react: 19.2.0 + react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) + + expo-glass-effect@55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): dependencies: expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) react: 19.2.0 react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) - expo-haptics@55.0.5(expo@55.0.0-preview.10): + expo-haptics@55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)): dependencies: expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) - expo-image@55.0.3(expo@55.0.0-preview.10)(react-native-web@0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): + expo-image@55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@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))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): dependencies: expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) react: 19.2.0 @@ -17719,14 +17971,19 @@ snapshots: optionalDependencies: react-native-web: 0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - expo-keep-awake@55.0.2(expo@55.0.0-preview.10)(react@19.2.0): + expo-keep-awake@55.0.2(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react@19.2.0): dependencies: expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) react: 19.2.0 + expo-keep-awake@55.0.2(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react@19.2.0): + dependencies: + expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + react: 19.2.0 + expo-linking@55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): dependencies: - expo-constants: 55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) + expo-constants: 55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) invariant: 2.2.4 react: 19.2.0 react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) @@ -17748,10 +18005,10 @@ snapshots: react: 19.2.0 react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) - expo-router@55.0.0-preview.7(cf9b05b365231229bf99876516ee7470): + expo-router@55.0.0-preview.7(mfjfgdrtx2hkscbkfatqrdcoli): dependencies: - '@expo/log-box': 55.0.6(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) - '@expo/metro-runtime': 55.0.5(expo@55.0.0-preview.10)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@expo/log-box': 55.0.6(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@expo/metro-runtime': 55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) '@expo/schema-utils': 55.0.2 '@radix-ui/react-slot': 1.2.0(@types/react@19.2.14)(react@19.2.0) '@radix-ui/react-tabs': 1.1.12(@types/react-dom@19.1.11(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -17762,12 +18019,12 @@ snapshots: debug: 4.4.1(supports-color@5.5.0) escape-string-regexp: 4.0.0 expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) - expo-constants: 55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) - expo-glass-effect: 55.0.5(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) - expo-image: 55.0.3(expo@55.0.0-preview.10)(react-native-web@0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo-constants: 55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) + expo-glass-effect: 55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo-image: 55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@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))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) expo-linking: 55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) expo-server: 55.0.3 - expo-symbols: 55.0.3(expo-font@55.0.3)(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo-symbols: 55.0.3(expo-font@55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@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 @@ -17797,11 +18054,15 @@ snapshots: - expo-font - supports-color + expo-secure-store@55.0.8(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)): + dependencies: + expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo-server@55.0.3: {} - expo-splash-screen@55.0.5(expo@55.0.0-preview.10): + expo-splash-screen@55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)): dependencies: - '@expo/prebuild-config': 55.0.4(expo@55.0.0-preview.10) + '@expo/prebuild-config': 55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)) expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) transitivePeerDependencies: - supports-color @@ -17812,16 +18073,16 @@ snapshots: react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) react-native-is-edge-to-edge: 1.2.1(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) - expo-symbols@55.0.3(expo-font@55.0.3)(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): + expo-symbols@55.0.3(expo-font@55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): dependencies: '@expo-google-fonts/material-symbols': 0.4.22 expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) - expo-font: 55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo-font: 55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) react: 19.2.0 react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) sf-symbols-typescript: 2.2.0 - expo-system-ui@55.0.5(expo@55.0.0-preview.10)(react-native-web@0.21.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)): + expo-system-ui@55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@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))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)): dependencies: '@react-native/normalize-colors': 0.83.1 debug: 4.4.1(supports-color@5.5.0) @@ -17832,7 +18093,7 @@ snapshots: transitivePeerDependencies: - supports-color - expo-web-browser@55.0.5(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)): + expo-web-browser@55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)): dependencies: expo: 55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) react-native: 0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) @@ -17840,23 +18101,23 @@ snapshots: expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.26.10 - '@expo/cli': 55.0.7(@expo/metro-runtime@55.0.5)(expo-constants@55.0.4)(expo-font@55.0.3)(expo-router@55.0.0-preview.7)(expo@55.0.0-preview.10)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@expo/cli': 55.0.7(u4h2ohzskzybnnlmfi7ygsjfkm) '@expo/config': 55.0.4 '@expo/config-plugins': 55.0.4 '@expo/devtools': 55.0.2(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) '@expo/fingerprint': 0.16.3 '@expo/local-build-cache-provider': 55.0.3 - '@expo/log-box': 55.0.6(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@expo/log-box': 55.0.6(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) '@expo/metro': 54.2.0 - '@expo/metro-config': 55.0.5(expo@55.0.0-preview.10) - '@expo/vector-icons': 15.0.3(expo-font@55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@expo/metro-config': 55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)) + '@expo/vector-icons': 15.0.3(expo-font@55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) '@ungap/structured-clone': 1.3.0 - babel-preset-expo: 55.0.4(@babel/core@7.28.0)(@babel/runtime@7.26.10)(expo@55.0.0-preview.10)(react-refresh@0.14.2) - expo-asset: 55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) - expo-constants: 55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) - expo-file-system: 55.0.5(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) - expo-font: 55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) - expo-keep-awake: 55.0.2(expo@55.0.0-preview.10)(react@19.2.0) + babel-preset-expo: 55.0.4(@babel/core@7.28.0)(@babel/runtime@7.26.10)(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-refresh@0.14.2) + expo-asset: 55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo-constants: 55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) + expo-file-system: 55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) + expo-font: 55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo-keep-awake: 55.0.2(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react@19.2.0) expo-modules-autolinking: 55.0.3 expo-modules-core: 55.0.8(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) pretty-format: 29.7.0 @@ -17865,8 +18126,8 @@ snapshots: react-refresh: 0.14.2 whatwg-url-minimum: 0.1.1 optionalDependencies: - '@expo/dom-webview': 55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) - '@expo/metro-runtime': 55.0.5(expo@55.0.0-preview.10)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@expo/dom-webview': 55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@expo/metro-runtime': 55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(expo-router@55.0.0-preview.7)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) transitivePeerDependencies: - '@babel/core' - bufferutil @@ -17880,23 +18141,23 @@ snapshots: expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.26.10 - '@expo/cli': 55.0.7(@expo/metro-runtime@55.0.5)(expo-constants@55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)))(expo-font@55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(expo@55.0.0-preview.10)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@expo/cli': 55.0.7(bt7yl4nwsruljbnpm4qr6emjei) '@expo/config': 55.0.4 '@expo/config-plugins': 55.0.4 '@expo/devtools': 55.0.2(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) '@expo/fingerprint': 0.16.3 '@expo/local-build-cache-provider': 55.0.3 - '@expo/log-box': 55.0.6(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@expo/log-box': 55.0.6(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) '@expo/metro': 54.2.0 - '@expo/metro-config': 55.0.5(expo@55.0.0-preview.10) - '@expo/vector-icons': 15.0.3(expo-font@55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@expo/metro-config': 55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)) + '@expo/vector-icons': 15.0.3(expo-font@55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) '@ungap/structured-clone': 1.3.0 - babel-preset-expo: 55.0.4(@babel/core@7.28.0)(@babel/runtime@7.26.10)(expo@55.0.0-preview.10)(react-refresh@0.14.2) - expo-asset: 55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) - expo-constants: 55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) - expo-file-system: 55.0.5(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) - expo-font: 55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) - expo-keep-awake: 55.0.2(expo@55.0.0-preview.10)(react@19.2.0) + babel-preset-expo: 55.0.4(@babel/core@7.28.0)(@babel/runtime@7.26.10)(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-refresh@0.14.2) + expo-asset: 55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo-constants: 55.0.4(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) + expo-file-system: 55.0.5(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0)) + expo-font: 55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo-keep-awake: 55.0.2(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react@19.2.0) expo-modules-autolinking: 55.0.3 expo-modules-core: 55.0.8(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) pretty-format: 29.7.0 @@ -17905,7 +18166,7 @@ snapshots: react-refresh: 0.14.2 whatwg-url-minimum: 0.1.1 optionalDependencies: - '@expo/dom-webview': 55.0.3(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + '@expo/dom-webview': 55.0.3(expo@55.0.0-preview.10(@babel/core@7.28.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) '@expo/metro-runtime': 55.0.5(expo@55.0.0-preview.10)(react-dom@19.2.0(react@19.2.0))(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) transitivePeerDependencies: - '@babel/core' @@ -18683,6 +18944,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + idb@8.0.3: {} + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -23093,18 +23356,18 @@ snapshots: - rollup - supports-color - vite-plugin-dts@4.5.4(@types/node@22.17.0)(rollup@4.44.1)(typescript@5.8.3)(vite@7.3.1(@types/node@22.17.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1)): + vite-plugin-dts@4.5.4(@types/node@22.17.0)(rollup@4.44.1)(typescript@5.9.3)(vite@7.3.1(@types/node@22.17.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1)): dependencies: '@microsoft/api-extractor': 7.48.1(@types/node@22.17.0) '@rollup/pluginutils': 5.2.0(rollup@4.44.1) '@volar/typescript': 2.4.17 - '@vue/language-core': 2.2.0(typescript@5.8.3) + '@vue/language-core': 2.2.0(typescript@5.9.3) compare-versions: 6.1.1 debug: 4.4.1(supports-color@5.5.0) kolorist: 1.8.0 local-pkg: 1.1.1 magic-string: 0.30.17 - typescript: 5.8.3 + typescript: 5.9.3 optionalDependencies: vite: 7.3.1(@types/node@22.17.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1) transitivePeerDependencies: diff --git a/website/src/docs/official-plugins/_meta.json b/website/src/docs/official-plugins/_meta.json index 9ccbac45..d1d15e97 100644 --- a/website/src/docs/official-plugins/_meta.json +++ b/website/src/docs/official-plugins/_meta.json @@ -44,6 +44,11 @@ "name": "mmkv", "label": "MMKV" }, + { + "type": "file", + "name": "storage", + "label": "Storage" + }, { "type": "file", "name": "require-profiler", diff --git a/website/src/docs/official-plugins/mmkv.mdx b/website/src/docs/official-plugins/mmkv.mdx index cad514a8..5d291d4e 100644 --- a/website/src/docs/official-plugins/mmkv.mdx +++ b/website/src/docs/official-plugins/mmkv.mdx @@ -6,6 +6,10 @@ import { PackageManagerTabs } from '@rspress/core/theme'; The MMKV plugin provides comprehensive storage inspection and management for React Native applications using MMKV, offering real-time data visualization and editing capabilities directly within your DevTools environment. +:::warning Deprecation Notice +The MMKV plugin is planned to be deprecated in favor of the generic [Storage plugin](./storage.md), which supports MMKV and additional storage backends through adapters. +::: + ## What is MMKV Plugin? The MMKV plugin is a powerful debugging tool that helps you inspect and manage MMKV storage instances in your React Native application. It provides: diff --git a/website/src/docs/official-plugins/overview.mdx b/website/src/docs/official-plugins/overview.mdx index 97f207d1..8f4956bb 100644 --- a/website/src/docs/official-plugins/overview.mdx +++ b/website/src/docs/official-plugins/overview.mdx @@ -69,6 +69,15 @@ Inspect and manage MMKV storage instances in your React Native app with comprehe - **Visual Data Representation**: Color-coded type indicators and formatted value display - **Data Management**: Add, edit, and delete entries directly from the DevTools interface +### [Storage](./storage.md) + +Inspect multiple storage backends from one panel using adapters for MMKV, AsyncStorage, and Expo SecureStore. This plugin provides: + +- **Multi-Adapter Support**: Combine multiple storage libraries in one DevTools panel +- **Per-Storage Capabilities**: Enforce supported value types per storage +- **Per-Storage Blacklist**: Hide sensitive or noisy keys per storage instance +- **Async + Sync Coverage**: Works with both sync (MMKV) and async (AsyncStorage/SecureStore) APIs + ## Installing Plugins Plugins should be installed as development dependencies since they are only needed during development: @@ -85,6 +94,8 @@ Plugins should be installed as development dependencies since they are only need + + See the individual plugin documentation for complete installation instructions including hook setup. ## Configuration @@ -101,4 +112,4 @@ Want to contribute to plugins or suggest new ones? Check out our [Plugin Develop --- -**Next**: Learn about the [Expo Atlas plugin](./expo-atlas.md), [TanStack Query plugin](./tanstack-query.md), [Network Activity Inspector](./network-activity.md), [Redux DevTools plugin](./redux-devtools.md), [Performance Monitor plugin](./performance-monitor.md), or [MMKV plugin](./mmkv.md). +**Next**: Learn about the [Expo Atlas plugin](./expo-atlas.md), [TanStack Query plugin](./tanstack-query.md), [Network Activity Inspector](./network-activity.md), [Redux DevTools plugin](./redux-devtools.md), [Performance Monitor plugin](./performance-monitor.md), [MMKV plugin](./mmkv.md), or [Storage plugin](./storage.md). diff --git a/website/src/docs/official-plugins/storage.mdx b/website/src/docs/official-plugins/storage.mdx new file mode 100644 index 00000000..28804b22 --- /dev/null +++ b/website/src/docs/official-plugins/storage.mdx @@ -0,0 +1,122 @@ +import { PackageManagerTabs } from '@rspress/core/theme'; + +# Storage Plugin + +The Storage plugin provides a generic storage inspector for React Native DevTools. It supports multiple adapters and multiple storages per adapter, so you can inspect MMKV, AsyncStorage, and SecureStore in one panel. + +## Installation + +Make sure to go through the [Getting Started guide](/docs/getting-started) before installing the plugin. + +Install the plugin: + + + +Install adapter peer dependencies for the storages you use: + + + +## Base Setup + +```ts title="App.tsx" +import { + createAsyncStorageAdapter, + createExpoSecureStorageAdapter, + createMMKVStorageAdapter, + useRozeniteStoragePlugin, +} from '@rozenite/storage-plugin'; + +const storages = [ + createMMKVStorageAdapter({ + storages: { + user: userStorage, + cache: cacheStorage, + }, + }), + createAsyncStorageAdapter({ + storage: AsyncStorage, + }), + createExpoSecureStorageAdapter({ + storage: SecureStore, + keys: ['token', 'session'], + }), +]; + +function App() { + useRozeniteStoragePlugin({ storages }); + return ; +} +``` + +## Adapter: MMKV + +```ts title="App.tsx" +createMMKVStorageAdapter({ + adapterId: 'mmkv', + adapterName: 'MMKV', + storages: { + 'user-storage': userStorage, + 'settings-storage': settingsStorage, + }, + blacklist: { + 'user-storage': /token|secret|password/, + }, +}); +``` + +### Limitations + +- MMKV v4 arrays are not supported because storage IDs are not readable from instances; pass a record (`{ id: instance }`) instead. +- Buffer support depends on MMKV runtime behavior and value decoding heuristics. + +## Adapter: AsyncStorage + +```ts title="App.tsx" +// v2 style +createAsyncStorageAdapter({ + storage: AsyncStorage, +}); + +// v3 style (instance-based) +createAsyncStorageAdapter({ + storages: { + auth: authStorageInstance, + cache: { + storage: cacheStorageInstance, + name: 'Cache Instance', + blacklist: /debug|temp/, + }, + }, +}); +``` + +## Adapter: Expo SecureStore + +```ts title="App.tsx" +createExpoSecureStorageAdapter({ + storage: SecureStore, + keys: async () => ['token', 'session', 'refreshToken'], + storageName: 'Auth Secure Storage', +}); +``` + +### Limitations + +- SecureStore does not provide key enumeration; you must provide known keys via `keys`. + +## Per-Storage Blacklist + +Blacklist is configured per storage and matched against the key in that storage. + +```ts title="App.tsx" +createAsyncStorageAdapter({ + storages: { + cache: { + storage: cacheStorageInstance, + blacklist: /temp|debug|internal/, + }, + }, +}); +``` + +**Next**: See [MMKV](./mmkv.md), [Network Activity](./network-activity.md), and [Plugin Development](../plugin-development/plugin-development.md).