From c047d36c640a871820c931b570437bb79001e655 Mon Sep 17 00:00:00 2001 From: IamLRBA Date: Mon, 2 Mar 2026 20:51:17 +0300 Subject: [PATCH 1/6] Tweak placeholder subtitle text and colors --- .../main/assets/webview/placeholder_app.html | 92 ++++++++++++++++++- formulus/assets/webview/placeholder_app.html | 92 ++++++++++++++++++- 2 files changed, 176 insertions(+), 8 deletions(-) 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 c8738a5b2..2bc5af3ad 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

@@ -183,7 +191,83 @@

Your Custom
App

} } - document.getElementById('login-btn').addEventListener('click', navigateToLogin); + function navigateToSync() { + try { + if (window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === 'function') { + window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'formulusNavigateToSync' })); + } + } catch (e) { + console.error('Placeholder: failed to post formulusNavigateToSync', e); + } + } + + var loginBtn = document.getElementById('login-btn'); + var subtitleEl = document.getElementById('subtitle-text'); + + function setSubtitleForMode(mode) { + if (!subtitleEl) return; + if (mode === 'sync') { + subtitleEl.textContent = 'Sync to load your forms'; + } else if (mode === 'update') { + subtitleEl.textContent = 'Update App Bundle to load your forms'; + } else { + subtitleEl.textContent = 'Login and Sync to load your forms'; + } + } + + function setButtonMode(mode) { + if (!loginBtn) return; + if (mode === 'sync') { + loginBtn.textContent = 'Sync Now'; + loginBtn.setAttribute('aria-label', 'Sync to load forms'); + loginBtn.onclick = navigateToSync; + } else if (mode === 'update') { + loginBtn.textContent = 'Update App Bundle'; + loginBtn.setAttribute( + 'aria-label', + 'Update app bundle to load your forms', + ); + // Updating the app bundle also happens on the Sync screen. + loginBtn.onclick = navigateToSync; + } else { + loginBtn.textContent = 'Login Now'; + loginBtn.setAttribute('aria-label', 'Login to load forms'); + loginBtn.onclick = navigateToLogin; + } + setSubtitleForMode(mode); + } + + function refreshLoginState() { + try { + var api = window.formulus || globalThis.formulus; + if (!api || typeof api.getCurrentUser !== 'function') { + setButtonMode('login'); + return; + } + api + .getCurrentUser() + .then(function(user) { + if (user && user.username) { + setButtonMode('sync'); + } else { + setButtonMode('login'); + } + }) + .catch(function() { + setButtonMode('login'); + }); + } catch (e) { + console.error('Placeholder: failed to refresh login state', e); + setButtonMode('login'); + } + } + + // Expose hooks so the native app can control the placeholder state + window.__formulusPlaceholderRefreshLoginState = refreshLoginState; + window.__formulusPlaceholderSetMode = setButtonMode; + + // Default state before we know anything about login + setButtonMode('login'); function waitForFormulus(cb, tries) { tries = tries !== undefined ? tries : 50; diff --git a/formulus/assets/webview/placeholder_app.html b/formulus/assets/webview/placeholder_app.html index c8738a5b2..2bc5af3ad 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

@@ -183,7 +191,83 @@

Your Custom
App

} } - document.getElementById('login-btn').addEventListener('click', navigateToLogin); + function navigateToSync() { + try { + if (window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === 'function') { + window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'formulusNavigateToSync' })); + } + } catch (e) { + console.error('Placeholder: failed to post formulusNavigateToSync', e); + } + } + + var loginBtn = document.getElementById('login-btn'); + var subtitleEl = document.getElementById('subtitle-text'); + + function setSubtitleForMode(mode) { + if (!subtitleEl) return; + if (mode === 'sync') { + subtitleEl.textContent = 'Sync to load your forms'; + } else if (mode === 'update') { + subtitleEl.textContent = 'Update App Bundle to load your forms'; + } else { + subtitleEl.textContent = 'Login and Sync to load your forms'; + } + } + + function setButtonMode(mode) { + if (!loginBtn) return; + if (mode === 'sync') { + loginBtn.textContent = 'Sync Now'; + loginBtn.setAttribute('aria-label', 'Sync to load forms'); + loginBtn.onclick = navigateToSync; + } else if (mode === 'update') { + loginBtn.textContent = 'Update App Bundle'; + loginBtn.setAttribute( + 'aria-label', + 'Update app bundle to load your forms', + ); + // Updating the app bundle also happens on the Sync screen. + loginBtn.onclick = navigateToSync; + } else { + loginBtn.textContent = 'Login Now'; + loginBtn.setAttribute('aria-label', 'Login to load forms'); + loginBtn.onclick = navigateToLogin; + } + setSubtitleForMode(mode); + } + + function refreshLoginState() { + try { + var api = window.formulus || globalThis.formulus; + if (!api || typeof api.getCurrentUser !== 'function') { + setButtonMode('login'); + return; + } + api + .getCurrentUser() + .then(function(user) { + if (user && user.username) { + setButtonMode('sync'); + } else { + setButtonMode('login'); + } + }) + .catch(function() { + setButtonMode('login'); + }); + } catch (e) { + console.error('Placeholder: failed to refresh login state', e); + setButtonMode('login'); + } + } + + // Expose hooks so the native app can control the placeholder state + window.__formulusPlaceholderRefreshLoginState = refreshLoginState; + window.__formulusPlaceholderSetMode = setButtonMode; + + // Default state before we know anything about login + setButtonMode('login'); function waitForFormulus(cb, tries) { tries = tries !== undefined ? tries : 50; From 2b8438d0124dae18b463df986704a2c4a565b208 Mon Sep 17 00:00:00 2001 From: IamLRBA Date: Mon, 2 Mar 2026 20:58:27 +0300 Subject: [PATCH 2/6] Stretch bottom nav fading line to full width --- formulus/src/navigation/MainTabNavigator.tsx | 68 +++++++++++--------- 1 file changed, 39 insertions(+), 29 deletions(-) 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(); From cff74335039ebfac331c464243d71012742363c3 Mon Sep 17 00:00:00 2001 From: IamLRBA Date: Tue, 3 Mar 2026 03:49:57 +0300 Subject: [PATCH 3/6] Formplayer and native modal Updates --- formulus-formplayer/src/App.tsx | 26 +- .../src/components/DraftSelector.tsx | 356 ++++++++++-------- .../src/components/FormLayout.tsx | 8 +- .../src/components/QuestionShell.tsx | 12 +- .../src/renderers/AdateQuestionRenderer.tsx | 13 +- .../src/renderers/SwipeLayoutRenderer.tsx | 111 +++++- formulus-formplayer/src/theme/theme.ts | 46 ++- formulus/src/components/FormplayerModal.tsx | 47 ++- 8 files changed, 404 insertions(+), 215 deletions(-) diff --git a/formulus-formplayer/src/App.tsx b/formulus-formplayer/src/App.tsx index 8ab18b5c0..1de6a4a10 100644 --- a/formulus-formplayer/src/App.tsx +++ b/formulus-formplayer/src/App.tsx @@ -545,6 +545,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); @@ -921,16 +928,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..3cebb2a4c 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,78 +128,136 @@ 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 => ( - - - - - - - - - Draft from {formatDate(draft.updatedAt)} - - } - label={getDraftAge(draft.updatedAt)} - size="small" - color={ - getDraftAge(draft.updatedAt) === 'recent' - ? 'success' - : getDraftAge(draft.updatedAt) === 'old' - ? 'warning' - : 'error' - } - sx={{ ml: 1 }} - /> - + + 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 && ( + + )} + + + Draft from {formatDate(draft.updatedAt)} + + } + label={age} + size="small" + color={chipColor} + sx={ + age === 'recent' + ? { + mt: 0.5, + bgcolor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + } + : { mt: 0.5 } + } + /> + + handleDeleteDraft(draft.id)} + size="small" + color="error" + sx={{ mt: 0.25 }}> + + + sx={{ mt: 1 }}> {draft.dataPreview} @@ -205,78 +268,76 @@ export const DraftSelector: React.FC = ({ <> • Editing observation: {draft.observationId} )} - - handleDeleteDraft(draft.id)} - size="small" - color="error" - sx={{ ml: 1 }}> - - + + - + ); + })} + + + + + ) : ( + + + No recent drafts found for this form. + + + )} - - - - - - ))} - - - ) : ( - - - 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 +351,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..c936fa7fd 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,86 @@ 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..847d48c54 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), @@ -281,8 +285,8 @@ 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 + ? `${primaryMain}20` + : `${primaryMain}14`, }, }, sizeSmall: { @@ -319,7 +323,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 +348,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 +391,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 +419,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 +460,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 +479,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 +518,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 +555,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), }, }, @@ -609,8 +613,8 @@ export const getThemeOptions = ( 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 + ? `${primaryMain}20` + : `${primaryMain}14`, }, }, sizeSmall: { @@ -635,7 +639,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/src/components/FormplayerModal.tsx b/formulus/src/components/FormplayerModal.tsx index e9e0b9e92..d34a8d90d 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'; @@ -548,11 +554,24 @@ const FormplayerModal = forwardRef( presentationStyle="fullScreen" statusBarTranslucent={false}> - + ( ? 'Edit Observation' : 'New Observation')} - Date: Tue, 3 Mar 2026 04:18:20 +0300 Subject: [PATCH 4/6] chore(formplayer): apply Prettier formatting for CI Made-with: Cursor --- .../src/components/DraftSelector.tsx | 189 +++++++++--------- .../src/renderers/SwipeLayoutRenderer.tsx | 5 +- formulus-formplayer/src/theme/theme.ts | 8 +- 3 files changed, 93 insertions(+), 109 deletions(-) diff --git a/formulus-formplayer/src/components/DraftSelector.tsx b/formulus-formplayer/src/components/DraftSelector.tsx index 3cebb2a4c..8f158f547 100644 --- a/formulus-formplayer/src/components/DraftSelector.tsx +++ b/formulus-formplayer/src/components/DraftSelector.tsx @@ -179,115 +179,109 @@ export const DraftSelector: React.FC = ({ width: '100%', maxWidth: 340, borderRadius: CONFIRM_CARD_RADIUS, - border: `${CONFIRM_BORDER_WIDTH}px solid`, - borderColor: 'divider', - padding: `${CONFIRM_CARD_PADDING}px`, - backgroundColor: alpha( - theme.palette.background.paper, - CONTAINER_ALPHA, - ), - overflow: 'hidden', - }}> - - - Available Drafts ({drafts.length}) - - - {drafts.map((draft, index) => { - const age = getDraftAge(draft.updatedAt); - const chipColor = - age === 'recent' - ? 'primary' - : age === 'old' - ? 'warning' - : 'error'; + + + 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 && ( - - )} - - - Draft from {formatDate(draft.updatedAt)} - - } - label={age} - size="small" - color={chipColor} - sx={ - age === 'recent' - ? { - mt: 0.5, - bgcolor: theme.palette.primary.main, - color: theme.palette.primary.contrastText, - } - : { mt: 0.5 } - } - /> + return ( + + {index > 0 && ( + + )} + + + Draft from {formatDate(draft.updatedAt)} + + } + label={age} + size="small" + color={chipColor} + sx={ + age === 'recent' + ? { + mt: 0.5, + bgcolor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + } + : { mt: 0.5 } + } + /> - handleDeleteDraft(draft.id)} - size="small" - color="error" - sx={{ mt: 0.25 }}> - - + handleDeleteDraft(draft.id)} + size="small" + color="error" + sx={{ mt: 0.25 }}> + + - - {draft.dataPreview} - + + {draft.dataPreview} + - - Created: {draft.createdAt.toLocaleDateString()}{' '} - {draft.createdAt.toLocaleTimeString()} - {draft.observationId && ( - <> • Editing observation: {draft.observationId} - )} - + + Created: {draft.createdAt.toLocaleDateString()}{' '} + {draft.createdAt.toLocaleTimeString()} + {draft.observationId && ( + <> • Editing observation: {draft.observationId} + )} + - + + - - ); - })} + ); + })} + - ) : ( - + No recent drafts found for this form. @@ -297,10 +291,7 @@ export const DraftSelector: React.FC = ({ {/* Start new form section – theme-aware */} - + Start Fresh diff --git a/formulus-formplayer/src/renderers/SwipeLayoutRenderer.tsx b/formulus-formplayer/src/renderers/SwipeLayoutRenderer.tsx index c936fa7fd..75dc32e53 100644 --- a/formulus-formplayer/src/renderers/SwipeLayoutRenderer.tsx +++ b/formulus-formplayer/src/renderers/SwipeLayoutRenderer.tsx @@ -590,10 +590,7 @@ const SwipeLayoutRenderer = ({ onPress={handleSnackbarClose}> Stay here - diff --git a/formulus-formplayer/src/theme/theme.ts b/formulus-formplayer/src/theme/theme.ts index 847d48c54..84719118b 100644 --- a/formulus-formplayer/src/theme/theme.ts +++ b/formulus-formplayer/src/theme/theme.ts @@ -284,9 +284,7 @@ export const getThemeOptions = ( }, text: { '&:hover': { - backgroundColor: isDark - ? `${primaryMain}20` - : `${primaryMain}14`, + backgroundColor: isDark ? `${primaryMain}20` : `${primaryMain}14`, }, }, sizeSmall: { @@ -612,9 +610,7 @@ export const getThemeOptions = ( minWidth: `${tokens.touchTarget.comfortable}px`, minHeight: `${tokens.touchTarget.comfortable}px`, '&:hover': { - backgroundColor: isDark - ? `${primaryMain}20` - : `${primaryMain}14`, + backgroundColor: isDark ? `${primaryMain}20` : `${primaryMain}14`, }, }, sizeSmall: { From 60e63c590dee3ec7b9f758bbe741b2b9218dd664 Mon Sep 17 00:00:00 2001 From: IamLRBA Date: Tue, 3 Mar 2026 05:22:27 +0300 Subject: [PATCH 5/6] Observations & Observation Detail: single header, blur to top, card styling --- .../src/components/common/ObservationCard.tsx | 27 +- formulus/src/navigation/MainAppNavigator.tsx | 81 ++- .../src/screens/ObservationDetailScreen.tsx | 465 +++++++++++------- 3 files changed, 378 insertions(+), 195 deletions(-) diff --git a/formulus/src/components/common/ObservationCard.tsx b/formulus/src/components/common/ObservationCard.tsx index 508532cdd..d06a7489b 100644 --- a/formulus/src/components/common/ObservationCard.tsx +++ b/formulus/src/components/common/ObservationCard.tsx @@ -69,7 +69,7 @@ const ObservationCard: React.FC = ({ 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/screens/ObservationDetailScreen.tsx b/formulus/src/screens/ObservationDetailScreen.tsx index 67d4d3560..1bbf817af 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,306 @@ const ObservationDetailScreen: React.FC = ({ ? JSON.parse(observation.data) : observation.data; - return ( - - - navigation.goBack()} - style={styles.backButton}> - - - Observation Details - -