diff --git a/package-lock.json b/package-lock.json index c50f492..d692d3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "@jcesarmobile/capacitor-ocr": "^0.3.0", "@meauxt/react-swipeable-list": "^1.0.0", "@mui/icons-material": "^5.16.13", - "@mui/joy": "^5.0.0-beta.20", + "@mui/joy": "5.0.0-beta.52", "@mui/material": "^5.15.2", "@openreplay/tracker": "^14.0.4", "@revenuecat/purchases-capacitor": "^12.0.0", diff --git a/package.json b/package.json index 28d443c..9c20283 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@jcesarmobile/capacitor-ocr": "^0.3.0", "@meauxt/react-swipeable-list": "^1.0.0", "@mui/icons-material": "^5.16.13", - "@mui/joy": "^5.0.0-beta.20", + "@mui/joy": "5.0.0-beta.52", "@mui/material": "^5.15.2", "@openreplay/tracker": "^14.0.4", "@revenuecat/purchases-capacitor": "^12.0.0", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 1a1056e..c1b378d 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -29,7 +29,8 @@ "filters": "Filters", "activities": "Activities", "points": "Points", - "settings": "Settings" + "settings": "Settings", + "reportBug": "Report a Bug" }, "search": { "title": "Search", diff --git a/src/components/common/AppModal.jsx b/src/components/common/AppModal.jsx index 55fefc6..72c84a7 100644 --- a/src/components/common/AppModal.jsx +++ b/src/components/common/AppModal.jsx @@ -2,7 +2,6 @@ import { Close } from '@mui/icons-material' import { Box, Divider, IconButton, Modal, Sheet, Typography } from '@mui/joy' import useMediaQuery from '@mui/material/useMediaQuery' import { forwardRef, useId } from 'react' -import { Z_INDEX } from '../../constants/zIndex' const WIDTH_BY_SIZE = { sm: 400, @@ -27,6 +26,7 @@ const AppModal = forwardRef( size = 'md', fullWidth = true, isMobile: isMobileProp, + keepMounted = false, mobilePresentation = 'sheet', role = 'dialog', showCloseButton = true, @@ -63,9 +63,16 @@ const AppModal = forwardRef( onClose={handleClose} aria-labelledby={titleId} aria-describedby={descriptionId} - keepMounted + keepMounted={keepMounted} sx={{ - zIndex: Z_INDEX.MODAL_BACKDROP, + // Joy raises portaled listboxes above modals, but its selector misses + // Menu because Menu renders with role="menu". Keep the workaround in + // the modal primitive so individual menus never need a z-index. + ...(open && { + '& ~ [role="menu"]': { + '--unstable_popup-zIndex': 'calc(var(--joy-zIndex-modal) + 1)', + }, + }), display: 'flex', alignItems: isSheet ? 'flex-end' : 'center', justifyContent: 'center', @@ -85,7 +92,6 @@ const AppModal = forwardRef( aria-describedby={descriptionId} variant='outlined' sx={{ - zIndex: Z_INDEX.MODAL_CONTENT, display: 'flex', flexDirection: 'column', width: isFullscreen diff --git a/src/constants/settingsSections.js b/src/constants/settingsSections.js new file mode 100644 index 0000000..3d64d8c --- /dev/null +++ b/src/constants/settingsSections.js @@ -0,0 +1,34 @@ +import { + AccountCircle, + Api, + Circle, + Code, + FamilyRestroom, + Language, + Notifications, + Palette, + Person, + Security, + Settings, + Storage, + ViewSidebar, +} from '@mui/icons-material' + +// Single source of truth for the settings sections: id, icon, and access +// gating. Titles/descriptions live in locales/settings.json under +// `overview.sections.`, keyed off the same ids. +export const SETTINGS_SECTIONS = [ + { id: 'profile', icon: Person }, + { id: 'circle', icon: Circle, parentOnly: true }, + { id: 'account', icon: AccountCircle, parentOnly: true }, + { id: 'subaccounts', icon: FamilyRestroom }, + { id: 'notifications', icon: Notifications }, + { id: 'mfa', icon: Security, parentOnly: true }, + { id: 'apitokens', icon: Api, parentOnly: true }, + { id: 'storage', icon: Storage }, + { id: 'sidepanel', icon: ViewSidebar }, + { id: 'theme', icon: Palette }, + { id: 'localization', icon: Language, isBeta: true }, + { id: 'advanced', icon: Settings }, + { id: 'developer', icon: Code }, +] diff --git a/src/constants/zIndex.js b/src/constants/zIndex.js deleted file mode 100644 index 59ab5e1..0000000 --- a/src/constants/zIndex.js +++ /dev/null @@ -1,38 +0,0 @@ -// Z-index constants for consistent layering -// Lower values appear behind higher values - -export const Z_INDEX = { - // Base layer (0-99) - BASE: 0, - CARD_OVERLAY: 1, - DROPDOWN_ITEM: 2, - TOOLTIP: 3, - - // UI Components (100-999) - SAFE_AREA: 100, - CALENDAR: 110, - SMART_INPUT: 110, - AUTOCOMPLETE: 200, - - // Navigation (1000-1999) - NAVBAR: 1000, - DRAWER: 999, - - // Modals and Overlays (2000-8999) - MODAL_BACKDROP: 2000, - MODAL_CONTENT: 2001, - MODAL_CLOSE_BUTTON: 2002, - // Popups that must float above open modals (portaled to document.body) - MODAL_POPOVER: 2100, - TOAST: 3000, - - // Critical System UI (9000-9999) - LOADING_SCREEN: 9000, - ALERTS: 9500, - NETWORK_BANNER: 9600, - - // Maximum (10000+) - Reserved for absolute emergencies - EMERGENCY: 10000, -} - -export default Z_INDEX diff --git a/src/search/GlobalSearchContext.jsx b/src/search/GlobalSearchContext.jsx index 3ebae49..eabc6ed 100644 --- a/src/search/GlobalSearchContext.jsx +++ b/src/search/GlobalSearchContext.jsx @@ -8,6 +8,7 @@ import { useMemo, useState, } from 'react' +import { useTranslation } from 'react-i18next' import { useLocation, useNavigate } from 'react-router-dom' import { offlineDB } from '../utils/OfflineDB' @@ -34,6 +35,7 @@ const uniqueBy = (items, getId) => [ export const GlobalSearchProvider = ({ children }) => { const queryClient = useQueryClient() + const { t } = useTranslation('settings') const location = useLocation() const navigate = useNavigate() const isMobile = useMediaQuery('(max-width:768px)') @@ -110,6 +112,7 @@ export const GlobalSearchProvider = ({ children }) => { labels, members, isParent: isParentUser(profile), + t, choresById: new Map(chores.map(item => [String(item.id), item])), projectsById: new Map(projects.map(item => [String(item.id), item])), membersById: new Map(members.map(item => [String(item.userId), item])), @@ -127,7 +130,7 @@ export const GlobalSearchProvider = ({ children }) => { } finally { setIsLoading(false) } - }, [queryClient]) + }, [queryClient, t]) const openSearch = useCallback( (query = '') => { diff --git a/src/search/searchProviders.js b/src/search/searchProviders.js index 7b61a54..13f5288 100644 --- a/src/search/searchProviders.js +++ b/src/search/searchProviders.js @@ -1,3 +1,5 @@ +import { SETTINGS_SECTIONS } from '../constants/settingsSections' + const stripHtml = value => { if (!value) return '' if (typeof globalThis.document === 'undefined') @@ -17,26 +19,6 @@ const HISTORY_STATUS = { 6: 'rescheduled', } -const SETTINGS = [ - ['profile', 'Profile', 'Name, avatar and personal details'], - ['circle', 'Circle', 'Members and household settings', true], - ['account', 'Account', 'Subscription and account management', true], - ['subaccounts', 'Subaccounts', 'Manage child accounts'], - ['notifications', 'Notifications', 'Reminders and notification preferences'], - ['mfa', 'Multi-factor authentication', 'Secure your account', true], - ['apitokens', 'API tokens', 'Manage integrations and access tokens', true], - ['storage', 'Storage', 'Files, backups and device storage'], - ['sidepanel', 'Side panel', 'Customize navigation'], - ['theme', 'Appearance', 'Theme, dark mode and colors'], - ['localization', 'Language and region', 'Language, dates and time formats'], - [ - 'advanced', - 'Advanced settings', - 'Offline support, webhooks and application behavior', - ], - ['developer', 'Developer settings', 'Diagnostics and experimental tools'], -] - const providers = [] export const registerSearchProvider = provider => { @@ -166,15 +148,15 @@ registerSearchProvider({ registerSearchProvider({ id: 'settings', - getDocuments: ({ isParent }) => - SETTINGS.filter(([, , , parentOnly]) => !parentOnly || isParent).map( - ([id, title, description]) => + getDocuments: ({ isParent, t }) => + SETTINGS_SECTIONS.filter(({ parentOnly }) => !parentOnly || isParent).map( + ({ id }) => document('settings', { id: `setting:${id}`, entityId: id, - title, + title: t(`overview.sections.${id}.title`), subtitle: 'Settings', - body: description, + body: t(`overview.sections.${id}.description`), keywords: `preferences configuration ${id}`, route: `/settings/${id}`, }), diff --git a/src/service/AlertsProvider.jsx b/src/service/AlertsProvider.jsx index 470c36a..62c2afd 100644 --- a/src/service/AlertsProvider.jsx +++ b/src/service/AlertsProvider.jsx @@ -1,7 +1,6 @@ import { Alert, Box } from '@mui/joy' import PropTypes from 'prop-types' import { createContext, useCallback, useContext, useState } from 'react' -import Z_INDEX from '../constants/zIndex' const FADE_DURATION = 400 // ms const ALERT_DURATION = 5000 // ms @@ -48,7 +47,7 @@ export const AlertsProvider = ({ children }) => { top: 0, left: 0, width: '100%', - zIndex: Z_INDEX.ALERTS, + zIndex: 'var(--joy-zIndex-snackbar)', overflow: 'hidden', }} > diff --git a/src/views/Chores/components/ChoreToolbarPrototype.jsx b/src/views/Chores/components/ChoreToolbarPrototype.jsx index 29960b4..cd4e28d 100644 --- a/src/views/Chores/components/ChoreToolbarPrototype.jsx +++ b/src/views/Chores/components/ChoreToolbarPrototype.jsx @@ -38,16 +38,17 @@ import { ButtonGroup, Chip, Divider, + Dropdown, IconButton, Input, Menu, + MenuButton, MenuItem, Typography, } from '@mui/joy' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useState } from 'react' import AppModal from '../../../components/common/AppModal' import ActiveFilterChips from '../../../components/common/filter/ActiveFilterChips' -import { Z_INDEX } from '../../../constants/zIndex' import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint' import { FILTER_COLORS } from '../../../utils/Colors' import Priorities from '../../../utils/Priorities' @@ -229,9 +230,8 @@ const ChoreToolbar = ({ const [localSelections, setLocalSelections] = useState(defaultSelections()) const [savingFilter, setSavingFilter] = useState(false) const [saveFilterName, setSaveFilterName] = useState('') - const [saveMenuAnchorEl, setSaveMenuAnchorEl] = useState(null) + const [saveMenuOpen, setSaveMenuOpen] = useState(false) const [editingSavedFilter, setEditingSavedFilter] = useState(null) - const saveMenuRef = useRef(null) const activeConditions = selectionsToConditions(localSelections) // ── badge counts ───────────────────────────────────────────────────────────── @@ -419,13 +419,13 @@ const ChoreToolbar = ({ } setSavingFilter(false) setSaveFilterName('') - setSaveMenuAnchorEl(null) + setSaveMenuOpen(false) setFilterSheetOpen(true) } useEffect(() => { if (!filterSheetOpen || savingFilter || activeConditions.length === 0) { - setSaveMenuAnchorEl(null) + setSaveMenuOpen(false) } }, [filterSheetOpen, savingFilter, activeConditions.length]) @@ -504,7 +504,7 @@ const ChoreToolbar = ({ onFilterSaved?.(editingSavedFilter.name) }) - setSaveMenuAnchorEl(null) + setSaveMenuOpen(false) setFilterSheetOpen(false) } @@ -681,7 +681,7 @@ const ChoreToolbar = ({ open={filterSheetOpen} isMobile onClose={() => { - setSaveMenuAnchorEl(null) + setSaveMenuOpen(false) setFilterSheetOpen(false) }} maxHeight='92vh' @@ -747,7 +747,7 @@ const ChoreToolbar = ({ disabled={!hasAnyActive && activeConditions.length === 0} onClick={() => { setLocalSelections(defaultSelections()) - setSaveMenuAnchorEl(null) + setSaveMenuOpen(false) onClearAllFilters?.() setFilterSheetOpen(false) }} @@ -756,33 +756,29 @@ const ChoreToolbar = ({ {activeConditions.length > 0 ? ( - <> + setSaveMenuOpen(isOpen)} + > - setSaveMenuAnchorEl(e.currentTarget)} > - + - setSaveMenuAnchorEl(null)} - placement='top-end' - sx={{ zIndex: Z_INDEX.MODAL_CONTENT + 10 }} - > + { - setSaveMenuAnchorEl(null) + setSaveMenuOpen(false) setSaveFilterName( editingSavedFilter ? `${editingSavedFilter.name} Copy` @@ -805,13 +801,13 @@ const ChoreToolbar = ({ Save as New Filter - + ) : (