diff --git a/formulus-formplayer/src/App.tsx b/formulus-formplayer/src/App.tsx index 052abd2fa..ecc20ad06 100644 --- a/formulus-formplayer/src/App.tsx +++ b/formulus-formplayer/src/App.tsx @@ -576,6 +576,13 @@ function App() { console.log( `Found ${availableDrafts.length} draft(s) for form ${receivedFormType}, showing draft selector`, ); + // Apply theme from params so draft selector respects light/dark mode + const params = initData.params; + const isDarkMode = params?.darkMode === true; + setDarkMode(isDarkMode); + if (params?.themeColors && typeof params.themeColors === 'object') { + setCustomThemeColors(params.themeColors as CustomThemeColors); + } setPendingFormInit(initData); setShowDraftSelector(true); setIsLoading(false); @@ -979,16 +986,19 @@ function App() { ); }, [customThemeColors]); - // Show draft selector if we have pending form init and available drafts + // Show draft selector if we have pending form init and available drafts. + // Wrap in ThemeProvider so DraftSelector gets the same theme (dark mode + custom colors). if (showDraftSelector && pendingFormInit) { return ( - + + + ); } diff --git a/formulus-formplayer/src/components/DraftSelector.tsx b/formulus-formplayer/src/components/DraftSelector.tsx index 5e78f93c9..8f158f547 100644 --- a/formulus-formplayer/src/components/DraftSelector.tsx +++ b/formulus-formplayer/src/components/DraftSelector.tsx @@ -9,9 +9,6 @@ import React, { useState, useEffect, useCallback } from 'react'; import { Box, Typography, - Card, - CardContent, - CardActions, IconButton, Dialog, DialogTitle, @@ -19,14 +16,14 @@ import { DialogActions, Alert, Chip, - Grid, Divider, + useTheme, } from '@mui/material'; +import { alpha } from '@mui/material/styles'; import { Button } from '@ode/components/react-web'; import { Delete as DeleteIcon, Schedule as ClockIcon, - Description as FormIcon, } from '@mui/icons-material'; import { draftService, DraftSummary } from '../services/DraftService'; @@ -45,6 +42,13 @@ interface DraftSelectorProps { fullScreen?: boolean; } +// Container style required +const CONFIRM_CARD_RADIUS = 0.7; +const CONFIRM_INNER_RADIUS = 0.7; +const CONFIRM_BORDER_WIDTH = 1; +const CONFIRM_CARD_PADDING = 16; +const CONTAINER_ALPHA = 0.4; + export const DraftSelector: React.FC = ({ formType, formVersion, @@ -53,6 +57,7 @@ export const DraftSelector: React.FC = ({ onClose, fullScreen = false, }) => { + const theme = useTheme(); const [drafts, setDrafts] = useState([]); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [draftToDelete, setDraftToDelete] = useState(null); @@ -123,160 +128,207 @@ export const DraftSelector: React.FC = ({ }; const content = ( - - {/* Header */} - - - Resume Draft or Start New - - - Form: {formType} - {formVersion && ( - - )} - - - - {/* Cleanup message */} - {cleanupMessage && ( - - {cleanupMessage} - - )} - - {/* Drafts list */} - {drafts.length > 0 ? ( + + + {/* Header – theme-aware */} - - Available Drafts ({drafts.length}) + + Resume Draft or Start New - - {drafts.map(draft => ( - - - - - + + Form: {formType} + {formVersion && ( + + )} + + + + {/* Cleanup message */} + {cleanupMessage && ( + + {cleanupMessage} + + )} + + {/* Available drafts – same outer + inner container style as Missing required fields dialog */} + {drafts.length > 0 ? ( + + + + + Available Drafts ({drafts.length}) + + + {drafts.map((draft, index) => { + const age = getDraftAge(draft.updatedAt); + const chipColor = + age === 'recent' + ? 'primary' + : age === 'old' + ? 'warning' + : 'error'; + + return ( + + {index > 0 && ( + + )} - - + sx={{ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + textAlign: 'center', + gap: 1, + }}> + Draft from {formatDate(draft.updatedAt)} } - label={getDraftAge(draft.updatedAt)} + label={age} size="small" - color={ - getDraftAge(draft.updatedAt) === 'recent' - ? 'success' - : getDraftAge(draft.updatedAt) === 'old' - ? 'warning' - : 'error' + color={chipColor} + sx={ + age === 'recent' + ? { + mt: 0.5, + bgcolor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + } + : { mt: 0.5 } } - sx={{ ml: 1 }} /> - - - {draft.dataPreview} - + handleDeleteDraft(draft.id)} + size="small" + color="error" + sx={{ mt: 0.25 }}> + + - - Created: {draft.createdAt.toLocaleDateString()}{' '} - {draft.createdAt.toLocaleTimeString()} - {draft.observationId && ( - <> • Editing observation: {draft.observationId} - )} - + + {draft.dataPreview} + + + + Created: {draft.createdAt.toLocaleDateString()}{' '} + {draft.createdAt.toLocaleTimeString()} + {draft.observationId && ( + <> • Editing observation: {draft.observationId} + )} + + + + + ); + })} + + + + + ) : ( + + + No recent drafts found for this form. + + + )} - handleDeleteDraft(draft.id)} - size="small" - color="error" - sx={{ ml: 1 }}> - - - - + - - - - - - ))} - - - ) : ( - - - No recent drafts found for this form. + {/* Start new form section – theme-aware */} + + + Start Fresh + + Begin a new form without any saved data. + + - )} - - - {/* Start new form section */} - - - Start Fresh - - - Begin a new form without any saved data. - - + {/* Delete confirmation dialog */} + setDeleteConfirmOpen(false)}> + Delete Draft + + + Are you sure you want to delete this draft? This action cannot be + undone. + + + + + + + - - {/* Delete confirmation dialog */} - setDeleteConfirmOpen(false)}> - Delete Draft - - - Are you sure you want to delete this draft? This action cannot be - undone. - - - - - - - ); @@ -290,24 +342,17 @@ export const DraftSelector: React.FC = ({ sx: { bgcolor: 'background.default', backgroundImage: 'none', + color: 'text.primary', }, }}> - - - Select Draft - {onClose && ( - - )} - + + + Select Draft + - {content} + + {content} + ); } diff --git a/formulus-formplayer/src/components/FormLayout.tsx b/formulus-formplayer/src/components/FormLayout.tsx index 589ba8e40..067eb9c61 100644 --- a/formulus-formplayer/src/components/FormLayout.tsx +++ b/formulus-formplayer/src/components/FormLayout.tsx @@ -163,7 +163,7 @@ const FormLayout: React.FC = ({ (previousButton || nextButton) && !isKeyboardVisible && ( ({ position: 'fixed', bottom: 0, @@ -181,10 +181,10 @@ const FormLayout: React.FC = ({ sm: `calc(${theme.spacing(1.5)} + env(safe-area-inset-bottom, 0px))`, md: `calc(${theme.spacing(1.5)} + env(safe-area-inset-bottom, 0px))`, }, - backgroundColor: 'background.paper', + backgroundColor: 'background.default', borderTop: 'none', - borderColor: 'divider', - boxShadow: `0 -4px 12px rgba(0,0,0,${(tokens as any).opacity?.['15'] ?? 0.15})`, + borderColor: 'transparent', + boxShadow: 'none', transition: 'opacity 0.2s ease-in-out, transform 0.2s ease-in-out', boxSizing: 'border-box', diff --git a/formulus-formplayer/src/components/QuestionShell.tsx b/formulus-formplayer/src/components/QuestionShell.tsx index 7e519f9ab..a5639c608 100644 --- a/formulus-formplayer/src/components/QuestionShell.tsx +++ b/formulus-formplayer/src/components/QuestionShell.tsx @@ -1,5 +1,6 @@ import React, { ReactNode } from 'react'; import { Box, Typography, Alert, Stack, Divider } from '@mui/material'; +import ErrorOutline from '@mui/icons-material/ErrorOutline'; /** * Simple HTML sanitizer that removes dangerous tags and attributes. @@ -115,7 +116,16 @@ const QuestionShell: React.FC = ({ )} {normalizedError && ( - + } + sx={{ + width: '100%', + mb: -1, + backgroundColor: 'transparent', + color: 'error.main', + '& .MuiAlert-icon': { color: 'error.main' }, + }}> {normalizedError} )} diff --git a/formulus-formplayer/src/renderers/AdateQuestionRenderer.tsx b/formulus-formplayer/src/renderers/AdateQuestionRenderer.tsx index 354bc2a5b..145b3c520 100644 --- a/formulus-formplayer/src/renderers/AdateQuestionRenderer.tsx +++ b/formulus-formplayer/src/renderers/AdateQuestionRenderer.tsx @@ -8,6 +8,7 @@ import { schemaMatches, } from '@jsonforms/core'; import { TextField, Box, Typography, Alert, Button } from '@mui/material'; +import ErrorOutline from '@mui/icons-material/ErrorOutline'; import { CalendarToday } from '@mui/icons-material'; import QuestionShell from '../components/QuestionShell'; import { tokens } from '../theme/tokens-adapter'; @@ -359,9 +360,17 @@ const AdateQuestionRenderer: React.FC = ({ )} - {/* Validation errors */} + {/* Validation errors – same as QuestionShell: icon, no background, text color matches icon */} {hasError && ( - + } + sx={{ + mt: 2, + backgroundColor: 'transparent', + color: 'error.main', + '& .MuiAlert-icon': { color: 'error.main' }, + }}> {Array.isArray(errors) ? errors.join(', ') : String(errors)} )} diff --git a/formulus-formplayer/src/renderers/SwipeLayoutRenderer.tsx b/formulus-formplayer/src/renderers/SwipeLayoutRenderer.tsx index a386167bf..75dc32e53 100644 --- a/formulus-formplayer/src/renderers/SwipeLayoutRenderer.tsx +++ b/formulus-formplayer/src/renderers/SwipeLayoutRenderer.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useState, useEffect, useMemo } from 'react'; +import { createPortal } from 'react-dom'; import { JsonFormsDispatch, withJsonFormsControlProps, @@ -11,7 +12,8 @@ import { RankedTester, } from '@jsonforms/core'; import { useSwipeable } from 'react-swipeable'; -import { Snackbar, Box, Typography } from '@mui/material'; +import { Box, Typography, useTheme } from '@mui/material'; +import { alpha } from '@mui/material/styles'; import { Button } from '@ode/components/react-web'; import { tokens } from '../theme/tokens-adapter'; import { useFormContext } from '../App'; @@ -118,6 +120,13 @@ export const groupAsSwipeLayoutTester: RankedTester = rankWith( // SwipeLayoutRenderer // --------------------------------------------------------------------------- +// Match ConfirmModal +const CONFIRM_CARD_RADIUS = 0.7; +const CONFIRM_INNER_RADIUS = 0.7; +const CONFIRM_BORDER_WIDTH = 1; +const CONFIRM_CARD_PADDING = 16; +const CONTAINER_ALPHA = 0.4; + const SwipeLayoutRenderer = ({ schema, uischema, @@ -130,6 +139,7 @@ const SwipeLayoutRenderer = ({ currentPage, onPageChange, }: SwipeLayoutProps) => { + const theme = useTheme(); const [isNavigating, setIsNavigating] = useState(false); const [snackbarOpen, setSnackbarOpen] = useState(false); const [pendingNavigation, setPendingNavigation] = useState( @@ -512,25 +522,83 @@ const SwipeLayoutRenderer = ({ )} - - Go Back - - } - anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} - sx={{ - '& .MuiSnackbarContent-root': { - backgroundColor: `rgba(0, 0, 0, ${(tokens as any).opacity?.['90'] ?? 0.9})`, - color: tokens.color.neutral.white, - boxShadow: (tokens as any).shadow?.portal?.md ?? tokens.shadow?.md, - }, - }} - /> + {snackbarOpen && + typeof document !== 'undefined' && + createPortal( + + + + + Missing required fields + + + {snackbarMessage || + 'Some required fields are missing. Any unsaved changes will be available as a draft when you return.'} + + + + + + + + , + document.body, + )} ); }; diff --git a/formulus-formplayer/src/theme/theme.ts b/formulus-formplayer/src/theme/theme.ts index 09a4e595e..84719118b 100644 --- a/formulus-formplayer/src/theme/theme.ts +++ b/formulus-formplayer/src/theme/theme.ts @@ -88,11 +88,15 @@ export const getThemeOptions = ( const c = (custom: string | undefined, fallback: string): string => custom ?? fallback; + // Effective primary for component overrides so custom app colors apply everywhere (radios, inputs, chips, etc.) + const primaryMain = c(customColors?.primary, tokens.color.brand.primary[500]); + const errorMain = c(customColors?.error, tokens.color.semantic.error[500]); + return { palette: { mode: mode, primary: { - main: c(customColors?.primary, tokens.color.brand.primary[500]), + main: c(customColors?.primary, primaryMain), light: c(customColors?.primaryLight, tokens.color.brand.primary[400]), dark: c(customColors?.primaryDark, tokens.color.brand.primary[600]), contrastText: c(customColors?.onPrimary, tokens.color.neutral.white), @@ -280,9 +284,7 @@ export const getThemeOptions = ( }, text: { '&:hover': { - backgroundColor: isDark - ? `${tokens.color.brand.primary[500]}20` // 12% opacity for dark mode - : `${tokens.color.brand.primary[500]}14`, // 8% opacity for light mode + backgroundColor: isDark ? `${primaryMain}20` : `${primaryMain}14`, }, }, sizeSmall: { @@ -319,7 +321,7 @@ export const getThemeOptions = ( : tokens.color.neutral[900], // Dark: #757575, Light: #212121 }, '&.Mui-focused fieldset': { - borderColor: tokens.color.brand.primary[500], + borderColor: primaryMain, borderWidth: parsePx(tokens.border.width.medium), // 2px on focus }, '&.Mui-error fieldset': { @@ -344,7 +346,7 @@ export const getThemeOptions = ( ? tokens.color.neutral[400] : tokens.color.neutral[600], // Dark: #BDBDBD, Light: #757575 '&.Mui-focused': { - color: tokens.color.brand.primary[500], + color: primaryMain, }, '&.Mui-error': { color: tokens.color.semantic.error[500], @@ -387,7 +389,7 @@ export const getThemeOptions = ( : tokens.color.neutral[900], }, '&.Mui-focused fieldset': { - borderColor: tokens.color.brand.primary[500], + borderColor: primaryMain, borderWidth: parsePx(tokens.border.width.medium), }, '&.Mui-error fieldset': { @@ -415,7 +417,7 @@ export const getThemeOptions = ( fontSize: parsePx(tokens.typography.fontSize.base), '&.Mui-focused': { '& .MuiOutlinedInput-notchedOutline': { - borderColor: tokens.color.brand.primary[500], + borderColor: primaryMain, borderWidth: parsePx(tokens.border.width.medium), }, }, @@ -456,7 +458,7 @@ export const getThemeOptions = ( ? tokens.color.neutral[500] : tokens.color.neutral[400], '&.Mui-checked': { - color: tokens.color.brand.primary[500], + color: primaryMain, }, '&.Mui-disabled': { color: isDark @@ -475,7 +477,7 @@ export const getThemeOptions = ( ? tokens.color.neutral[500] : tokens.color.neutral[400], '&.Mui-checked': { - color: tokens.color.brand.primary[500], + color: primaryMain, }, '&.Mui-disabled': { color: isDark @@ -514,7 +516,7 @@ export const getThemeOptions = ( transform: `translateX(${tokens.spacing?.[5] ?? '20px'})`, color: tokens.color.neutral.white, '& + .MuiSwitch-track': { - backgroundColor: tokens.color.brand.primary[500], + backgroundColor: primaryMain, opacity: 1, border: 0, }, @@ -551,7 +553,7 @@ export const getThemeOptions = ( minHeight: `${tokens.touchTarget.large}px`, '&.Mui-focused': { '& .MuiOutlinedInput-notchedOutline': { - borderColor: tokens.color.brand.primary[500], + borderColor: primaryMain, borderWidth: parsePx(tokens.border.width.medium), }, }, @@ -608,9 +610,7 @@ export const getThemeOptions = ( minWidth: `${tokens.touchTarget.comfortable}px`, minHeight: `${tokens.touchTarget.comfortable}px`, '&:hover': { - backgroundColor: isDark - ? `${tokens.color.brand.primary[500]}20` // 12% opacity for dark mode - : `${tokens.color.brand.primary[500]}14`, // 8% opacity for light mode + backgroundColor: isDark ? `${primaryMain}20` : `${primaryMain}14`, }, }, sizeSmall: { @@ -635,7 +635,21 @@ export const getThemeOptions = ( styleOverrides: { root: { borderRadius: parsePx(tokens.border.radius.sm), - backgroundColor: isDark ? tokens.color.neutral[800] : undefined, // Dark: #424242 for alerts (matches paper) + }, + standardError: { + backgroundColor: 'transparent', + color: errorMain, + '& .MuiAlert-icon': { color: errorMain }, + }, + }, + }, + MuiFormHelperText: { + styleOverrides: { + root: { + '&.Mui-error': { + backgroundColor: 'transparent', + color: errorMain, + }, }, }, }, diff --git a/formulus/android/app/src/main/assets/webview/placeholder_app.html b/formulus/android/app/src/main/assets/webview/placeholder_app.html index efa2094c5..8924bedfe 100644 --- a/formulus/android/app/src/main/assets/webview/placeholder_app.html +++ b/formulus/android/app/src/main/assets/webview/placeholder_app.html @@ -106,7 +106,7 @@ margin: 0 0 var(--spacing-8); } [data-theme="dark"] .placeholder-subtitle { - color: var(--color-neutral-white); + color: var(--color-neutral-400); } [data-theme="light"] .placeholder-subtitle { color: var(--color-neutral-600); @@ -161,8 +161,16 @@

Your Custom
App

-

Login and Sync to load your forms!

- +

+ Login and Sync to load your forms +

+

Secure • Fast • Offline

v0.1.0-native

diff --git a/formulus/assets/webview/placeholder_app.html b/formulus/assets/webview/placeholder_app.html index efa2094c5..8924bedfe 100644 --- a/formulus/assets/webview/placeholder_app.html +++ b/formulus/assets/webview/placeholder_app.html @@ -106,7 +106,7 @@ margin: 0 0 var(--spacing-8); } [data-theme="dark"] .placeholder-subtitle { - color: var(--color-neutral-white); + color: var(--color-neutral-400); } [data-theme="light"] .placeholder-subtitle { color: var(--color-neutral-600); @@ -161,8 +161,16 @@

Your Custom
App

-

Login and Sync to load your forms!

- +

+ Login and Sync to load your forms +

+

Secure • Fast • Offline

v0.1.0-native

diff --git a/formulus/src/components/FormplayerModal.tsx b/formulus/src/components/FormplayerModal.tsx index 3f0cff742..101c2f179 100644 --- a/formulus/src/components/FormplayerModal.tsx +++ b/formulus/src/components/FormplayerModal.tsx @@ -32,6 +32,12 @@ import { import { databaseService } from '../database'; import colors from '../theme/colors'; +import { + odeSpacing, + odeTypography, + odeBorderWidth, + odeRadius, +} from '../theme/odeDesign'; import { FormSpec } from '../services'; // FormService will be imported directly import { ExtensionService } from '../services/ExtensionService'; import RNFS from 'react-native-fs'; @@ -599,11 +605,24 @@ const FormplayerModal = forwardRef( presentationStyle="fullScreen" statusBarTranslucent={false}> - + ( ? 'Edit Observation' : 'New Observation')} - = ({ size={24} color={ isSynced - ? colors.semantic.success[500] + ? (themeColors.primary as string) : colors.semantic.warning[500] } /> @@ -90,15 +90,6 @@ const ObservationCard: React.FC = ({ {dateStr} at {timeStr} - - - {isSynced ? 'Synced' : 'Pending'} - - {`By ${ @@ -192,22 +183,6 @@ const styles = StyleSheet.create({ fontSize: 12, color: colors.neutral[500], }, - statusBadge: { - paddingHorizontal: 8, - paddingVertical: 2, - borderRadius: 10, - }, - syncedBadge: { - backgroundColor: colors.semantic.success[50], - }, - pendingBadge: { - backgroundColor: colors.semantic.warning[50], - }, - statusText: { - fontSize: 11, - fontWeight: '500', - color: colors.neutral[900], - }, actions: { flexDirection: 'row', gap: 8, diff --git a/formulus/src/navigation/MainAppNavigator.tsx b/formulus/src/navigation/MainAppNavigator.tsx index b54f46d93..1f16aa6f5 100644 --- a/formulus/src/navigation/MainAppNavigator.tsx +++ b/formulus/src/navigation/MainAppNavigator.tsx @@ -1,15 +1,84 @@ import React, { useEffect, useState } from 'react'; +import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; import { createStackNavigator } from '@react-navigation/stack'; import { useFocusEffect } from '@react-navigation/native'; +import Icon from '@react-native-vector-icons/material-design-icons'; import MainTabNavigator from './MainTabNavigator'; import WelcomeScreen from '../screens/WelcomeScreen'; import ObservationDetailScreen from '../screens/ObservationDetailScreen'; import { MainAppStackParamList } from '../types/NavigationTypes'; import { serverConfigService } from '../services/ServerConfigService'; import { useAppTheme } from '../contexts/AppThemeContext'; +import { + odeSpacing, + odeRadius, + odeBorderWidth, + odeTypography, +} from '../theme/odeDesign'; const Stack = createStackNavigator(); +function ObservationDetailHeader({ + navigation, + themeColors, +}: { + navigation: { goBack: () => void }; + themeColors: Record; +}) { + return ( + + navigation.goBack()} + style={observationDetailHeaderStyles.backBtn}> + + + + Observation Details + + + + ); +} + +const observationDetailHeaderStyles = StyleSheet.create({ + wrapper: { + flexDirection: 'row', + alignItems: 'center', + marginHorizontal: odeSpacing.sm, + padding: odeSpacing.md, + borderWidth: odeBorderWidth.hairline, + borderBottomWidth: odeBorderWidth.hairline, + borderBottomLeftRadius: odeRadius.card, + borderBottomRightRadius: odeRadius.card, + overflow: 'hidden', + }, + backBtn: { + padding: odeSpacing.xxs, + marginRight: odeSpacing.xs, + }, + title: { + flex: 1, + fontSize: odeTypography.screenTitle, + fontWeight: 'bold', + textAlign: 'center', + }, + placeholder: { + width: 24 + odeSpacing.xxs * 2 + odeSpacing.xs, + }, +}); + const MainAppNavigator: React.FC = () => { const [isConfigured, setIsConfigured] = useState(null); @@ -60,7 +129,17 @@ const MainAppNavigator: React.FC = () => { ( + + ), + }} /> ); diff --git a/formulus/src/navigation/MainTabNavigator.tsx b/formulus/src/navigation/MainTabNavigator.tsx index f904c4d77..480691fbb 100644 --- a/formulus/src/navigation/MainTabNavigator.tsx +++ b/formulus/src/navigation/MainTabNavigator.tsx @@ -1,5 +1,11 @@ import React from 'react'; -import { View, StyleSheet, Platform, Pressable } from 'react-native'; +import { + View, + StyleSheet, + Platform, + Pressable, + useWindowDimensions, +} from 'react-native'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { CommonActions } from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -137,35 +143,39 @@ const TAB_ICONS: Record< const isVisibleMainTab = (value: string): value is VisibleMainTab => (VISIBLE_MAIN_TABS as readonly string[]).includes(value); -const FadingTopLine = ({ borderColor }: { borderColor: string }) => ( - - - - - - - - - - { + const { width } = useWindowDimensions(); + + return ( + - -); + width={width} + style={[styles.fadingLineSvg, { height: tabBarTokens.topLineHeight }]} + preserveAspectRatio="none"> + + + + + + + + + + + ); +}; const TabBarBackground = () => { const { themeColors, resolvedMode } = useAppTheme(); diff --git a/formulus/src/screens/ObservationDetailScreen.tsx b/formulus/src/screens/ObservationDetailScreen.tsx index 67d4d3560..d5298b0ff 100644 --- a/formulus/src/screens/ObservationDetailScreen.tsx +++ b/formulus/src/screens/ObservationDetailScreen.tsx @@ -4,7 +4,6 @@ import { Text, StyleSheet, ScrollView, - TouchableOpacity, Alert, ActivityIndicator, } from 'react-native'; @@ -14,10 +13,12 @@ import { Observation } from '../database/models/Observation'; import { FormService } from '../services/FormService'; import { openFormplayerFromNative } from '../webview/FormulusMessageHandlers'; import { useNavigation } from '@react-navigation/native'; -import colors from '../theme/colors'; +import colors, { withAlpha, CONTAINER_ALPHA } from '../theme/colors'; import { Button } from '../components/common'; import { useAppTheme } from '../contexts/AppThemeContext'; import { useConfirmModal } from '../contexts/ConfirmModalContext'; +import BlurredScreenBackground from '../components/BlurredScreenBackground'; +import { odeSpacing, odeRadius } from '../theme/odeDesign'; interface ObservationDetailScreenProps { route: { @@ -142,7 +143,9 @@ const ObservationDetailScreen: React.FC = ({ key={key} style={[styles.fieldContainer, { paddingLeft: level * 16 }]}> {key}: - null + + null + ); } @@ -184,29 +187,40 @@ const ObservationDetailScreen: React.FC = ({ key={key} style={[styles.fieldContainer, { paddingLeft: level * 16 }]}> {key}: - {String(value)} + + {String(value)} + ); }; if (loading) { return ( - - - - Loading observation... - - + + + + + + Loading observation... + + + + ); } if (!observation) { return ( - - - Observation not found - - + + + + Observation not found + + + ); } @@ -218,193 +232,307 @@ const ObservationDetailScreen: React.FC = ({ ? JSON.parse(observation.data) : observation.data; - return ( - - - navigation.goBack()} - style={styles.backButton}> - - - Observation Details - -