diff --git a/example/App.tsx b/example/App.tsx index a26e540..d44ecc5 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Pressable, StyleSheet, Text, View } from 'react-native'; +import { Pressable, StatusBar, StyleSheet, Text, View } from 'react-native'; import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; import { SalveDbProvider } from '@salve-software/react-native-salve-db'; import Salvetron from '@salve-software/salvetron-react-native'; @@ -14,34 +14,28 @@ import { InfiniteQueryScreen } from './src/screens/InfiniteQueryScreen'; import { BenchmarkScreen } from './src/screens/BenchmarkScreen'; import { SyncTestScreen } from './src/screens/SyncTestScreen'; import { LoginScreen, type LoginTokens } from './src/screens/LoginScreen'; -import { ResetControls } from './src/components/ResetControls'; +import { SideMenu } from './src/components/SideMenu'; +import { IconButton, ToastProvider } from './src/components/ui'; +import { colors, spacing } from './src/theme/tokens'; +import { WalletIcon, StackIcon, LightningIcon, ArrowsClockwiseIcon, ListIcon } from './src/theme/icons'; import { SYNC_SERVER_BASE_URL } from './src/library/syncServer'; if (__DEV__) { Salvetron.connect({ host: 'localhost', port: 8765 }); } -const ACCENT = '#5B5FEF'; - const TABS = [ - { key: 'expenses', label: 'Query', icon: '💸' }, - { key: 'infinite', label: 'Infinite Query', icon: '📜' }, - { key: 'benchmark', label: 'Benchmark', icon: '⚡' }, - { key: 'sync', label: 'Sync Test', icon: '🔄' }, + { key: 'expenses', label: 'Query', Icon: WalletIcon }, + { key: 'infinite', label: 'Infinite Query', Icon: StackIcon }, + { key: 'benchmark', label: 'Benchmark', Icon: LightningIcon }, + { key: 'sync', label: 'Sync Test', Icon: ArrowsClockwiseIcon }, ] as const; type TabKey = (typeof TABS)[number]['key']; -const SCHEMAS = [ - ExpenseSchema, - BudgetSchema, - BenchmarkSchema, - FeedItemSchema, - UserSchema, - ProductSchema, -]; +const SCHEMAS = [ExpenseSchema, BudgetSchema, BenchmarkSchema, FeedItemSchema, UserSchema, ProductSchema]; -/** Shared by the initial SalveDbProvider mount and ResetControls' "reconfigure" button — same db name both times, so the connection (and its subscriptions) survives a reset(). */ +/** Shared by the initial SalveDbProvider mount and SideMenu's "reconfigure" button — same db name both times, so the connection (and its subscriptions) survives a reset(). */ function buildDbConfig(tokens: LoginTokens) { return { name: 'salve-db-example', @@ -66,10 +60,14 @@ interface AppTabsProps { function AppTabs({ tokens, onLogout }: AppTabsProps): React.JSX.Element { const [tab, setTab] = useState('expenses'); + const [menuOpen, setMenuOpen] = useState(false); return ( - buildDbConfig(tokens)} onLogout={onLogout} /> + + Salve DB + } onPress={() => setMenuOpen(true)} /> + {tab === 'expenses' ? : null} @@ -79,13 +77,27 @@ function AppTabs({ tokens, onLogout }: AppTabsProps): React.JSX.Element { - {TABS.map(({ key, label, icon }) => ( - setTab(key)} style={styles.tabItem}> - {icon} - {label} - - ))} + {TABS.map(({ key, label, Icon }) => { + const active = tab === key; + return ( + setTab(key)} style={styles.tabItem}> + {active ? : null} + + + {label} + + + ); + })} + + setMenuOpen(false)} + schemas={SCHEMAS} + buildConfig={() => buildDbConfig(tokens)} + onLogout={onLogout} + /> ); } @@ -93,19 +105,18 @@ function AppTabs({ tokens, onLogout }: AppTabsProps): React.JSX.Element { function App(): React.JSX.Element { const [tokens, setTokens] = useState(null); - if (tokens === null) { - return ( - - - - ); - } - return ( - - setTokens(null)} /> - + + + {tokens === null ? ( + + ) : ( + + setTokens(null)} /> + + )} + ); } @@ -115,33 +126,50 @@ export default App; const styles = StyleSheet.create({ flex: { flex: 1, + backgroundColor: colors.canvas, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: spacing.lg, + paddingVertical: spacing.sm, + backgroundColor: colors.surface, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: colors.line, + }, + headerTitle: { + fontSize: 16, + fontWeight: '700', + color: colors.ink, }, tabBar: { flexDirection: 'row', - backgroundColor: '#FFFFFF', + backgroundColor: colors.surface, borderTopWidth: StyleSheet.hairlineWidth, - borderTopColor: '#E1E2F0', + borderTopColor: colors.line, }, tabItem: { flex: 1, alignItems: 'center', - paddingTop: 10, - paddingBottom: 6, - gap: 2, - }, - tabIcon: { - fontSize: 20, - opacity: 0.5, + paddingTop: spacing.sm, + paddingBottom: spacing.xs, + gap: spacing.xxs, }, - tabIconActive: { - opacity: 1, + tabIndicator: { + position: 'absolute', + top: 0, + width: 24, + height: 2, + borderRadius: 1, + backgroundColor: colors.accent, }, tabLabel: { - fontSize: 11, + fontSize: 10, fontWeight: '600', - color: '#9B9DB8', + color: colors.muted, }, tabLabelActive: { - color: ACCENT, + color: colors.accent, }, }); diff --git a/example/global.d.ts b/example/global.d.ts new file mode 100644 index 0000000..61df4bb --- /dev/null +++ b/example/global.d.ts @@ -0,0 +1,19 @@ +// Module augmentation required by phosphor-react-native's TypeScript setup — +// see the "Typescript support" section of its README. Declares only the +// extra `className` member each interface is missing; merges into the +// existing declaration rather than re-declaring it (an `extends` here would +// make the interface reference itself). +import 'react-native-svg'; +import 'phosphor-react-native'; + +declare module 'react-native-svg' { + interface SvgProps { + className?: string; + } +} + +declare module 'phosphor-react-native' { + interface IconProps { + className?: string; + } +} diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 9f59529..354d1fa 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1939,6 +1939,51 @@ PODS: - React-utils (= 0.86.0) - ReactNativeDependencies - ReactNativeDependencies (0.86.0) + - RNSVG (15.15.5): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - RNSVG/common (= 15.15.5) + - Yoga + - RNSVG/common (15.15.5): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga - SalveDb (0.1.1): - hermes-engine - NitroModules @@ -2044,6 +2089,7 @@ DEPENDENCIES: - ReactCodegen (from `build/generated/ios/ReactCodegen`) - ReactCommon/turbomodule/core (from `../../node_modules/react-native/ReactCommon`) - ReactNativeDependencies (from `../../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec`) + - RNSVG (from `../../node_modules/react-native-svg`) - SalveDb (from `../..`) - Yoga (from `../../node_modules/react-native/ReactCommon/yoga`) @@ -2203,6 +2249,8 @@ EXTERNAL SOURCES: :path: "../../node_modules/react-native/ReactCommon" ReactNativeDependencies: :podspec: "../../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec" + RNSVG: + :path: "../../node_modules/react-native-svg" SalveDb: :path: "../.." Yoga: @@ -2286,6 +2334,7 @@ SPEC CHECKSUMS: ReactCodegen: 1973e629fe2614a7b2cf72919cb9935b6c34675f ReactCommon: d5c1bb4427bf51c443de5926aac332c89ddd9363 ReactNativeDependencies: fa0a54b3f5319ae0e3b9aff32bfee7a424b88e66 + RNSVG: 6cae8d4082913be2eb43eaec2a456222c42195ed SalveDb: c9d225fdca0b6ee1bf26697372566eac2aa0640f Yoga: fe50ab299e578f397fef753cf309c6703a4db29b diff --git a/example/ios/SalveDbExample.xcodeproj/project.pbxproj b/example/ios/SalveDbExample.xcodeproj/project.pbxproj index 9ce05e7..ef5f9c7 100644 --- a/example/ios/SalveDbExample.xcodeproj/project.pbxproj +++ b/example/ios/SalveDbExample.xcodeproj/project.pbxproj @@ -11,17 +11,17 @@ 6D150186035001D7357AA846 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; }; 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; - C097E6C4B0ADD3EC7F80A6E8 /* libPods-SalveDbExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F8F2B860BF69F7821E07C3D /* libPods-SalveDbExample.a */; }; + C6E915F3DF6C60DE686559A7 /* libPods-SalveDbExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 49E666747AF051099A41A290 /* libPods-SalveDbExample.a */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 10655584FE167515188A1060 /* Pods-SalveDbExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SalveDbExample.debug.xcconfig"; path = "Target Support Files/Pods-SalveDbExample/Pods-SalveDbExample.debug.xcconfig"; sourceTree = ""; }; + 0A7B746496E6D15E07A7307F /* Pods-SalveDbExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SalveDbExample.release.xcconfig"; path = "Target Support Files/Pods-SalveDbExample/Pods-SalveDbExample.release.xcconfig"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* SalveDbExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SalveDbExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = SalveDbExample/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = SalveDbExample/Info.plist; sourceTree = ""; }; 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = SalveDbExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; - 1F8F2B860BF69F7821E07C3D /* libPods-SalveDbExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SalveDbExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 3A5618B54CF630B74C215667 /* Pods-SalveDbExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SalveDbExample.release.xcconfig"; path = "Target Support Files/Pods-SalveDbExample/Pods-SalveDbExample.release.xcconfig"; sourceTree = ""; }; + 1C6E7DDF126616369256D989 /* Pods-SalveDbExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SalveDbExample.debug.xcconfig"; path = "Target Support Files/Pods-SalveDbExample/Pods-SalveDbExample.debug.xcconfig"; sourceTree = ""; }; + 49E666747AF051099A41A290 /* libPods-SalveDbExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SalveDbExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = SalveDbExample/AppDelegate.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = SalveDbExample/LaunchScreen.storyboard; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; @@ -32,7 +32,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C097E6C4B0ADD3EC7F80A6E8 /* libPods-SalveDbExample.a in Frameworks */, + C6E915F3DF6C60DE686559A7 /* libPods-SalveDbExample.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -55,7 +55,7 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - 1F8F2B860BF69F7821E07C3D /* libPods-SalveDbExample.a */, + 49E666747AF051099A41A290 /* libPods-SalveDbExample.a */, ); name = Frameworks; sourceTree = ""; @@ -92,8 +92,8 @@ BBD78D7AC51CEA395F1C20DB /* Pods */ = { isa = PBXGroup; children = ( - 10655584FE167515188A1060 /* Pods-SalveDbExample.debug.xcconfig */, - 3A5618B54CF630B74C215667 /* Pods-SalveDbExample.release.xcconfig */, + 1C6E7DDF126616369256D989 /* Pods-SalveDbExample.debug.xcconfig */, + 0A7B746496E6D15E07A7307F /* Pods-SalveDbExample.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -105,13 +105,13 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "SalveDbExample" */; buildPhases = ( - C62BB49E30CCF2A01D9B41FC /* [CP] Check Pods Manifest.lock */, + 81270F069B86D64C1C9F9C4F /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, - FFE2D58645D2744BF96615B9 /* [CP] Embed Pods Frameworks */, - 4B7D259B78BA68639FEB64C1 /* [CP] Copy Pods Resources */, + 8076584F57D7BAF8DB7AEDAA /* [CP] Embed Pods Frameworks */, + 67DE7510A084AA7FAAEDBF1E /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -183,7 +183,7 @@ shellPath = /bin/sh; shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"\\\"$WITH_ENVIRONMENT\\\" \\\"$REACT_NATIVE_XCODE\\\"\"\n"; }; - 4B7D259B78BA68639FEB64C1 /* [CP] Copy Pods Resources */ = { + 67DE7510A084AA7FAAEDBF1E /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -191,60 +191,52 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SalveDbExample/Pods-SalveDbExample-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-SalveDbExample/Pods-SalveDbExample-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SalveDbExample/Pods-SalveDbExample-resources.sh\"\n"; showEnvVarsInLog = 0; }; - C62BB49E30CCF2A01D9B41FC /* [CP] Check Pods Manifest.lock */ = { + 8076584F57D7BAF8DB7AEDAA /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-SalveDbExample/Pods-SalveDbExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-SalveDbExample-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-SalveDbExample/Pods-SalveDbExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SalveDbExample/Pods-SalveDbExample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - FFE2D58645D2744BF96615B9 /* [CP] Embed Pods Frameworks */ = { + 81270F069B86D64C1C9F9C4F /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SalveDbExample/Pods-SalveDbExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "[CP] Embed Pods Frameworks"; + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-SalveDbExample/Pods-SalveDbExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-SalveDbExample-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SalveDbExample/Pods-SalveDbExample-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -263,7 +255,7 @@ /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 10655584FE167515188A1060 /* Pods-SalveDbExample.debug.xcconfig */; + baseConfigurationReference = 1C6E7DDF126616369256D989 /* Pods-SalveDbExample.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -294,7 +286,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 3A5618B54CF630B74C215667 /* Pods-SalveDbExample.release.xcconfig */; + baseConfigurationReference = 0A7B746496E6D15E07A7307F /* Pods-SalveDbExample.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; diff --git a/example/package.json b/example/package.json index 0b335c7..601199d 100644 --- a/example/package.json +++ b/example/package.json @@ -16,10 +16,12 @@ "dependencies": { "@react-native/new-app-screen": "0.86.0", "@salve-software/salvetron-react-native": "^1.1.5", + "phosphor-react-native": "^3.0.6", "react": "19.2.8", "react-native": "0.86.0", "react-native-nitro-modules": "0.36.1", - "react-native-safe-area-context": "^5.5.2" + "react-native-safe-area-context": "^5.5.2", + "react-native-svg": "^15.15.5" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/example/src/components/ResetControls.tsx b/example/src/components/ResetControls.tsx deleted file mode 100644 index 94e670b..0000000 --- a/example/src/components/ResetControls.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import React, { useState } from 'react'; -import { ActivityIndicator, Pressable, StyleSheet, Text, View } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; -import type { AnySchema } from '@salve-software/react-native-salve-db'; -import { Database } from '@salve-software/react-native-salve-db'; - -const ACCENT = '#5B5FEF'; -const DANGER = '#E14F62'; - -type ConfigureProps = Parameters[0]; - -interface ResetControlsProps { - schemas: AnySchema[]; - buildConfig: () => ConfigureProps; - /** Called after a successful `Database.logout()`, so the caller can drop its held tokens and show the login screen again. */ - onLogout: () => void; -} - -/** - * Manual QA surface for Database.reset(): "Reset (local)" proves the wipe + - * reactivity without touching sync; "Reset + Reconfigure" additionally calls - * configure() again with the same db name, proving the connection (and every - * useQuery subscription mounted anywhere in the tab tree) survives instead of - * silently going stale. - */ -export function ResetControls({ schemas, buildConfig, onLogout }: ResetControlsProps): React.JSX.Element { - const [busy, setBusy] = useState(false); - const [lastResult, setLastResult] = useState(null); - - async function registerAllSchemas() { - for (const schema of schemas) { - await Database.register({ schema }); - } - } - - async function resetLocal() { - setBusy(true); - setLastResult(null); - try { - await Database.reset(); - await registerAllSchemas(); - setLastResult('Reset (local) ok — data wiped, sync needs configure() again to resume.'); - } catch (err) { - setLastResult(`Error: ${err instanceof Error ? err.message : String(err)}`); - } finally { - setBusy(false); - } - } - - async function resetAndReconfigure() { - setBusy(true); - setLastResult(null); - try { - await Database.reset(); - Database.configure(buildConfig()); - await registerAllSchemas(); - setLastResult('Reset + Reconfigure ok — same db name, sync restored, no remount.'); - } catch (err) { - setLastResult(`Error: ${err instanceof Error ? err.message : String(err)}`); - } finally { - setBusy(false); - } - } - - async function logout() { - setBusy(true); - setLastResult(null); - try { - Database.logout(); - onLogout(); - } catch (err) { - setLastResult(`Error: ${err instanceof Error ? err.message : String(err)}`); - } finally { - setBusy(false); - } - } - - return ( - - - - Reset (local) - - - Reset + Reconfigure - - - Logout - - - {busy ? : null} - {lastResult ? ( - - {lastResult} - - ) : null} - - ); -} - -const styles = StyleSheet.create({ - container: { - paddingHorizontal: 16, - paddingTop: 8, - paddingBottom: 10, - backgroundColor: '#FFFFFF', - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: '#E1E2F0', - }, - buttonRow: { - flexDirection: 'row', - gap: 8, - }, - button: { - flex: 1, - height: 34, - borderRadius: 10, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: '#F4F5FA', - }, - buttonDanger: { - backgroundColor: DANGER, - }, - buttonDisabled: { - opacity: 0.5, - }, - buttonText: { - fontSize: 12, - fontWeight: '700', - color: '#1C1D3E', - }, - buttonDangerText: { - fontSize: 12, - fontWeight: '700', - color: '#FFFFFF', - }, - spinner: { - marginTop: 6, - }, - resultText: { - marginTop: 6, - fontSize: 11, - color: '#4A4D7A', - fontWeight: '500', - }, - errorText: { - color: DANGER, - }, -}); diff --git a/example/src/components/SideMenu.tsx b/example/src/components/SideMenu.tsx new file mode 100644 index 0000000..668d1b9 --- /dev/null +++ b/example/src/components/SideMenu.tsx @@ -0,0 +1,180 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { ActivityIndicator, Animated, Dimensions, Modal, Pressable, StyleSheet, Text, View } from 'react-native'; +import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; +import type { AnySchema } from '@salve-software/react-native-salve-db'; +import { Database } from '@salve-software/react-native-salve-db'; +import { Button, IconButton, Divider } from './ui'; +import { XIcon, SignOutIcon, DatabaseIcon } from '../theme/icons'; +import { colors, spacing, typography } from '../theme/tokens'; + +const PANEL_WIDTH = Math.min(300, Dimensions.get('window').width * 0.82); + +type ConfigureProps = Parameters[0]; + +interface SideMenuProps { + visible: boolean; + onClose: () => void; + schemas: AnySchema[]; + buildConfig: () => ConfigureProps; + /** Called after a successful `Database.logout()`, so the caller can drop its held tokens and show the login screen again. */ + onLogout: () => void; +} + +/** + * Replaces the always-visible ResetControls bar: the same reset/reconfigure/ + * logout QA actions, now tucked behind a hamburger trigger in a slide-in + * panel instead of permanently occupying screen space above every tab. + */ +export function SideMenu({ visible, onClose, schemas, buildConfig, onLogout }: SideMenuProps): React.JSX.Element { + const [busy, setBusy] = useState(false); + const [lastResult, setLastResult] = useState(null); + const translateX = useRef(new Animated.Value(PANEL_WIDTH)).current; + + useEffect(() => { + Animated.timing(translateX, { + toValue: visible ? 0 : PANEL_WIDTH, + duration: 200, + useNativeDriver: true, + }).start(); + }, [visible, translateX]); + + async function registerAllSchemas() { + for (const schema of schemas) { + await Database.register({ schema }); + } + } + + async function resetLocal() { + setBusy(true); + setLastResult(null); + try { + await Database.reset(); + await registerAllSchemas(); + setLastResult('Reset (local) ok — data wiped, sync needs configure() again to resume.'); + } catch (err) { + setLastResult(`Error: ${err instanceof Error ? err.message : String(err)}`); + } finally { + setBusy(false); + } + } + + async function resetAndReconfigure() { + setBusy(true); + setLastResult(null); + try { + await Database.reset(); + Database.configure(buildConfig()); + await registerAllSchemas(); + setLastResult('Reset + Reconfigure ok — same db name, sync restored, no remount.'); + } catch (err) { + setLastResult(`Error: ${err instanceof Error ? err.message : String(err)}`); + } finally { + setBusy(false); + } + } + + async function logout() { + setBusy(true); + setLastResult(null); + try { + Database.logout(); + onClose(); + onLogout(); + } catch (err) { + setLastResult(`Error: ${err instanceof Error ? err.message : String(err)}`); + } finally { + setBusy(false); + } + } + + return ( + + + + + + + Menu + } onPress={onClose} /> + + + + + {buildConfig().name} + + + + + +