Merge pull request #205 from donetick/localization-rtl

Localization rtl
This commit is contained in:
Mohamad Tarbin
2026-08-11 02:11:35 -04:00
committed by GitHub
8 changed files with 194 additions and 118 deletions

42
package-lock.json generated
View File

@@ -32,6 +32,7 @@
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@emotion/cache": "^11.14.0",
"@emotion/react": "^11.11.3", "@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
@@ -77,6 +78,8 @@
"reactjs-social-login": "^2.6.3", "reactjs-social-login": "^2.6.3",
"recharts": "^2.15.0", "recharts": "^2.15.0",
"reusify": "^1.0.4", "reusify": "^1.0.4",
"stylis": "^4.4.0",
"stylis-plugin-rtl": "^2.1.1",
"tesseract.js": "^7.0.0", "tesseract.js": "^7.0.0",
"vite-plugin-pwa": "^0.20.0" "vite-plugin-pwa": "^0.20.0"
}, },
@@ -2311,8 +2314,16 @@
"stylis": "4.2.0" "stylis": "4.2.0"
} }
}, },
"node_modules/@emotion/babel-plugin/node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"license": "MIT"
},
"node_modules/@emotion/cache": { "node_modules/@emotion/cache": {
"version": "11.14.0", "version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@emotion/memoize": "^0.9.0", "@emotion/memoize": "^0.9.0",
@@ -2322,6 +2333,12 @@
"stylis": "4.2.0" "stylis": "4.2.0"
} }
}, },
"node_modules/@emotion/cache/node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"license": "MIT"
},
"node_modules/@emotion/hash": { "node_modules/@emotion/hash": {
"version": "0.9.2", "version": "0.9.2",
"license": "MIT" "license": "MIT"
@@ -7480,6 +7497,15 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/cssjanus": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/cssjanus/-/cssjanus-2.3.1.tgz",
"integrity": "sha512-gWZQ/S0tthU2KCc55C5zbjxQN0XPK1sT0qufCqwCNUauGIsPDOfdBMBWosP+Kd4wf2A2Nxm/z6JAP6yNGnS7QQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"license": "MIT" "license": "MIT"
@@ -14063,9 +14089,23 @@
} }
}, },
"node_modules/stylis": { "node_modules/stylis": {
"version": "4.2.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
"integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/stylis-plugin-rtl": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/stylis-plugin-rtl/-/stylis-plugin-rtl-2.1.1.tgz",
"integrity": "sha512-q6xIkri6fBufIO/sV55md2CbgS5c6gg9EhSVATtHHCdOnbN/jcI0u3lYhNVeuI65c4lQPo67g8xmq5jrREvzlg==",
"license": "MIT",
"dependencies": {
"cssjanus": "^2.0.1"
},
"peerDependencies": {
"stylis": "4.x"
}
},
"node_modules/sucrase": { "node_modules/sucrase": {
"version": "3.35.0", "version": "3.35.0",
"dev": true, "dev": true,

View File

@@ -64,6 +64,7 @@
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@emotion/cache": "^11.14.0",
"@emotion/react": "^11.11.3", "@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
@@ -109,6 +110,8 @@
"reactjs-social-login": "^2.6.3", "reactjs-social-login": "^2.6.3",
"recharts": "^2.15.0", "recharts": "^2.15.0",
"reusify": "^1.0.4", "reusify": "^1.0.4",
"stylis": "^4.4.0",
"stylis-plugin-rtl": "^2.1.1",
"tesseract.js": "^7.0.0", "tesseract.js": "^7.0.0",
"vite-plugin-pwa": "^0.20.0" "vite-plugin-pwa": "^0.20.0"
}, },

View File

@@ -8,8 +8,10 @@ import ThemeContext from './ThemeContext'
const Contexts = ({ children }) => { const Contexts = ({ children }) => {
const contexts = [ const contexts = [
AlertsProvider, AlertsProvider,
ThemeContext, // Above ThemeContext: the theme reads the active language to pick the
// text direction and the matching emotion cache.
LocalizationProvider, LocalizationProvider,
ThemeContext,
QueryContext, QueryContext,
NotificationProvider, NotificationProvider,
RouterContext, RouterContext,

View File

@@ -1,8 +1,9 @@
import useStickyState from '@/hooks/useStickyState'
import moment from 'moment' import moment from 'moment'
import { createContext, useContext, useEffect } from 'react' import { createContext, useContext, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useStickyState from '@/hooks/useStickyState'
const LocalizationContext = createContext() const LocalizationContext = createContext()
export const DATE_FORMATS = { export const DATE_FORMATS = {
@@ -21,6 +22,7 @@ export const TIME_FORMATS = {
export const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur'] export const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur']
export const AVAILABLE_LANGUAGES = [ export const AVAILABLE_LANGUAGES = [
{ code: 'ar', name: 'Arabic', nativeName: 'العربية' },
{ code: 'de', name: 'German', nativeName: 'Deutsch' }, { code: 'de', name: 'German', nativeName: 'Deutsch' },
{ code: 'en', name: 'English', nativeName: 'English' }, { code: 'en', name: 'English', nativeName: 'English' },
{ code: 'es', name: 'Spanish', nativeName: 'Español' }, { code: 'es', name: 'Spanish', nativeName: 'Español' },

View File

@@ -1,9 +1,16 @@
import createCache from '@emotion/cache'
import { CacheProvider } from '@emotion/react'
import { CssBaseline } from '@mui/joy' import { CssBaseline } from '@mui/joy'
import { CssVarsProvider, extendTheme } from '@mui/joy/styles' import { CssVarsProvider, extendTheme } from '@mui/joy/styles'
import PropType from 'prop-types' import PropType from 'prop-types'
import { useMemo } from 'react'
import { prefixer } from 'stylis'
import rtlPlugin from 'stylis-plugin-rtl'
import { COLORS, THEME_BACKGROUND } from '@/constants/theme' import { COLORS, THEME_BACKGROUND } from '@/constants/theme'
import { useLocalization } from './LocalizationContext'
const primaryColor = 'cyan' const primaryColor = 'cyan'
const shades = [ const shades = [
'50', '50',
@@ -24,7 +31,7 @@ const primaryPalette = getPalette(primaryColor)
const CONTROL_RADIUS = '12px' const CONTROL_RADIUS = '12px'
const ICON_BUTTON_RADIUS = '10px' const ICON_BUTTON_RADIUS = '10px'
const theme = extendTheme({ const themeConfig = {
radius: { radius: {
xs: '6px', xs: '6px',
sm: '8px', sm: '8px',
@@ -140,20 +147,93 @@ const theme = extendTheme({
}, },
JoyButtonGroup: { JoyButtonGroup: {
styleOverrides: { styleOverrides: {
root: { root: ({ ownerState, theme }) => ({
'--ButtonGroup-radius': CONTROL_RADIUS, '--ButtonGroup-radius': CONTROL_RADIUS,
...(theme.direction === 'rtl' && buttonGroupRtlGeometry(ownerState)),
}),
},
},
// ToggleButtonGroup is styled(StyledButtonGroup) under its own slot name, so
// it inherits the same geometry — and the same RTL problem — but not the
// JoyButtonGroup override. Its radius is left at Joy's default on purpose;
// only the direction-sensitive geometry needs correcting.
JoyToggleButtonGroup: {
styleOverrides: {
root: ({ ownerState, theme }) =>
theme.direction === 'rtl' ? buttonGroupRtlGeometry(ownerState) : {},
}, },
}, },
}, },
}, }
})
const ThemeContext = ({ children }) => ( // Joy packs a ButtonGroup's direction-sensitive geometry into CSS custom
// properties: --Button-radius as a four-corner shorthand, --Button-margin as the
// negative overlap that collapses the seam between siblings. stylis-plugin-rtl
// mirrors real properties only — it cannot know what a custom property will end
// up feeding — so those two survive into RTL still in LTR order, while the
// separator borders declared alongside them *do* flip. The result is rounded
// corners on the wrong ends and the overlap pulling the wrong way. Re-mirror
// them here. Vertical groups have no horizontal geometry, so they are left be.
const GROUP_RADIUS = 'var(--ButtonGroup-radius)'
const CHILD_RADIUS = 'var(--unstable_childRadius)'
const OVERLAP = 'calc(var(--ButtonGroup-separatorSize) * -1)'
// Corners read clockwise from top-left. [data-first-child] is the DOM-first
// button, which in RTL renders at the *right* end of the group, so it is the one
// that needs its right corners rounded — and vice versa for [data-last-child].
const ROUNDED_RIGHT = `${CHILD_RADIUS} ${GROUP_RADIUS} ${GROUP_RADIUS} ${CHILD_RADIUS}`
const ROUNDED_LEFT = `${GROUP_RADIUS} ${CHILD_RADIUS} ${CHILD_RADIUS} ${GROUP_RADIUS}`
const buttonGroupRtlGeometry = ownerState => {
if (ownerState.orientation === 'vertical') return {}
return {
'& > [data-first-child]': {
'--Button-radius': ROUNDED_RIGHT,
'--IconButton-radius': ROUNDED_RIGHT,
},
'& > [data-last-child]': {
'--Button-radius': ROUNDED_LEFT,
'--IconButton-radius': ROUNDED_LEFT,
},
// Each non-first button overlaps the sibling to its right in RTL.
'& > :not([data-first-child]):not(:only-child)': {
'--Button-margin': `0 ${OVERLAP} 0 0`,
'--IconButton-margin': `0 ${OVERLAP} 0 0`,
},
}
}
// One theme and one emotion cache per direction, built once and reused. The RTL
// cache runs every rule through stylis-plugin-rtl, which mirrors the physical
// properties (margin-left, left, text-align, translateX, …) that `sx` emits, so
// components written for LTR lay out correctly without per-component overrides.
const byDirection = {
ltr: {
theme: extendTheme({ ...themeConfig, direction: 'ltr' }),
cache: createCache({ key: 'dt', stylisPlugins: [prefixer] }),
},
rtl: {
theme: extendTheme({ ...themeConfig, direction: 'rtl' }),
cache: createCache({ key: 'dt-rtl', stylisPlugins: [prefixer, rtlPlugin] }),
},
}
const ThemeContext = ({ children }) => {
const { isRTL } = useLocalization()
const { cache, theme } = useMemo(
() => byDirection[isRTL ? 'rtl' : 'ltr'],
[isRTL],
)
return (
<CacheProvider value={cache}>
<CssVarsProvider theme={theme}> <CssVarsProvider theme={theme}>
<CssBaseline /> <CssBaseline />
{children} {children}
</CssVarsProvider> </CssVarsProvider>
) </CacheProvider>
)
}
ThemeContext.propTypes = { ThemeContext.propTypes = {
children: PropType.node, children: PropType.node,

View File

@@ -55,69 +55,17 @@ html {
will-change: auto; will-change: auto;
} }
/* RTL Support */ /*
[dir='rtl'] { * RTL Support
direction: rtl; *
text-align: right; * Component styling is mirrored by stylis-plugin-rtl (see ThemeContext), which
} * flips the physical properties emitted by `sx`. Do not add per-component
* `[dir='rtl']` overrides here — they fight the plugin and double-flip.
[dir='rtl'] .rtl-mirror { *
transform: scaleX(-1); * Only opt-in utilities belong in this block:
} * .rtl-flip — mirror an icon that encodes a direction (arrows, chevrons).
* Do NOT use it on symbols that read the same either way.
/* Handle margins and paddings for RTL */ */
[dir='rtl'] .ml-auto {
margin-left: 0;
margin-right: auto;
}
[dir='rtl'] .mr-auto {
margin-right: 0;
margin-left: auto;
}
/* Flip icons and arrows in RTL */
[dir='rtl'] .rtl-flip { [dir='rtl'] .rtl-flip {
transform: scaleX(-1); transform: scaleX(-1);
} }
/* Ensure proper text alignment in RTL */
[dir='rtl'] input,
[dir='rtl'] textarea {
text-align: right;
}
/* Handle border radius for RTL */
[dir='rtl'] .rounded-l-none {
border-radius: 0 0.375rem 0.375rem 0;
}
[dir='rtl'] .rounded-r-none {
border-radius: 0.375rem 0 0 0.375rem;
}
/* Fix flex alignment for RTL */
[dir='rtl'] .flex {
direction: rtl;
}
/* Ensure cards and containers align properly in RTL */
[dir='rtl'] .MuiCard-root,
[dir='rtl'] .MuiBox-root,
[dir='rtl'] .MuiStack-root {
text-align: right;
}
/* Fix list item alignment in RTL */
[dir='rtl'] .MuiListItem-root,
[dir='rtl'] .MuiListItemButton-root {
flex-direction: row-reverse;
}
/* Fix gap alignment in RTL */
[dir='rtl'] .gap-1,
[dir='rtl'] .gap-2,
[dir='rtl'] .gap-3,
[dir='rtl'] .gap-4 {
direction: rtl;
}

View File

@@ -24,6 +24,7 @@ import {
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import AppModal from '../components/common/AppModal' import AppModal from '../components/common/AppModal'
@@ -38,15 +39,6 @@ const GROUPS = [
'settings', 'settings',
'actions', 'actions',
] ]
const GROUP_LABELS = {
tasks: 'Tasks',
history: 'Notes',
projects: 'Projects',
labels: 'Labels',
people: 'People',
settings: 'Settings',
actions: 'Quick actions',
}
const ICONS = { const ICONS = {
tasks: <CheckCircleOutline />, tasks: <CheckCircleOutline />,
@@ -58,33 +50,33 @@ const ICONS = {
actions: <AddRounded />, actions: <AddRounded />,
} }
const QUICK_ACTIONS = [ const buildQuickActions = t => [
{ {
id: 'action:create', id: 'action:create',
provider: 'actions', provider: 'actions',
title: 'Create a task', title: t('search.actions.createTask'),
subtitle: 'Quick action', subtitle: t('search.actions.quickAction'),
route: '/chores/create', route: '/chores/create',
}, },
{ {
id: 'action:tasks', id: 'action:tasks',
provider: 'actions', provider: 'actions',
title: 'View all tasks', title: t('search.actions.viewAllTasks'),
subtitle: 'Navigation', subtitle: t('search.actions.navigation'),
route: '/chores', route: '/chores',
}, },
{ {
id: 'action:archived', id: 'action:archived',
provider: 'actions', provider: 'actions',
title: 'View archived tasks', title: t('search.actions.viewArchivedTasks'),
subtitle: 'Navigation', subtitle: t('search.actions.navigation'),
route: '/archived', route: '/archived',
}, },
{ {
id: 'action:settings', id: 'action:settings',
provider: 'actions', provider: 'actions',
title: 'Open settings', title: t('search.actions.openSettings'),
subtitle: 'Navigation', subtitle: t('search.actions.navigation'),
route: '/settings', route: '/settings',
}, },
] ]
@@ -142,6 +134,8 @@ const Highlight = ({ query, text }) => {
} }
const SearchContainer = ({ children, onClose, presentation }) => { const SearchContainer = ({ children, onClose, presentation }) => {
const { t } = useTranslation()
if (presentation === 'page') { if (presentation === 'page') {
return ( return (
<Box <Box
@@ -165,7 +159,7 @@ const SearchContainer = ({ children, onClose, presentation }) => {
open open
onClose={onClose} onClose={onClose}
disableRestoreFocus disableRestoreFocus
title='Search' title={t('search.title')}
size='lg' size='lg'
maxHeight='min(720px, calc(100dvh - 48px))' maxHeight='min(720px, calc(100dvh - 48px))'
contentSx={{ contentSx={{
@@ -189,6 +183,7 @@ const GlobalSearchPalette = ({
presentation = 'modal', presentation = 'modal',
}) => { }) => {
const navigate = useNavigate() const navigate = useNavigate()
const { t } = useTranslation()
const focusInputRef = useCallback(node => { const focusInputRef = useCallback(node => {
if (node) requestAnimationFrame(() => node.focus()) if (node) requestAnimationFrame(() => node.focus())
}, []) }, [])
@@ -232,7 +227,7 @@ const GlobalSearchPalette = ({
const recentResults = recents const recentResults = recents
.map(item => currentById.get(item.id) || item) .map(item => currentById.get(item.id) || item)
.filter(item => item.provider !== 'history' || currentById.has(item.id)) .filter(item => item.provider !== 'history' || currentById.has(item.id))
return [...recentResults, ...QUICK_ACTIONS] return [...recentResults, ...buildQuickActions(t)]
} }
const grouped = GROUPS.filter(group => group !== 'actions').flatMap(group => const grouped = GROUPS.filter(group => group !== 'actions').flatMap(group =>
@@ -258,12 +253,12 @@ const GlobalSearchPalette = ({
grouped.push({ grouped.push({
id: 'action:filter-tasks', id: 'action:filter-tasks',
provider: 'actions', provider: 'actions',
title: `Show tasks matching “${query.trim()}`, title: t('search.actions.filterTasks', { query: query.trim() }),
subtitle: 'Filter the task list', subtitle: t('search.actions.filterTasksSubtitle'),
route: `/chores?search=${encodeURIComponent(query.trim())}`, route: `/chores?search=${encodeURIComponent(query.trim())}`,
}) })
return grouped return grouped
}, [documents, query, recents, searchIndexes]) }, [documents, query, recents, searchIndexes, t])
useEffect(() => { useEffect(() => {
selectedResultRef.current?.scrollIntoView({ selectedResultRef.current?.scrollIntoView({
@@ -302,8 +297,7 @@ const GlobalSearchPalette = ({
slotProps={{ slotProps={{
input: { input: {
ref: focusInputRef, ref: focusInputRef,
'aria-label': 'aria-label': t('search.inputAriaLabel'),
'Search tasks, history, projects, labels and settings',
}, },
}} }}
value={query} value={query}
@@ -312,14 +306,14 @@ const GlobalSearchPalette = ({
setSelectedIndex(0) setSelectedIndex(0)
}} }}
onKeyDown={onInputKeyDown} onKeyDown={onInputKeyDown}
placeholder='Search Donetick' placeholder={t('search.placeholder')}
startDecorator={<SearchRounded />} startDecorator={<SearchRounded />}
endDecorator={ endDecorator={
isLoading ? ( isLoading ? (
<CircularProgress size='sm' /> <CircularProgress size='sm' />
) : presentation === 'modal' ? ( ) : presentation === 'modal' ? (
<Chip size='sm' variant='outlined'> <Chip size='sm' variant='outlined'>
Esc {t('search.escape')}
</Chip> </Chip>
) : null ) : null
} }
@@ -333,7 +327,7 @@ const GlobalSearchPalette = ({
level='body-xs' level='body-xs'
sx={{ color: 'text.tertiary', mt: 1, px: 0.5 }} sx={{ color: 'text.tertiary', mt: 1, px: 0.5 }}
> >
Searching content available on this device {t('search.deviceNote')}
</Typography> </Typography>
</Box> </Box>
<Divider /> <Divider />
@@ -350,9 +344,9 @@ const GlobalSearchPalette = ({
<InboxOutlined <InboxOutlined
sx={{ fontSize: 36, color: 'text.tertiary', mb: 1 }} sx={{ fontSize: 36, color: 'text.tertiary', mb: 1 }}
/> />
<Typography level='title-md'>No direct matches</Typography> <Typography level='title-md'>{t('search.empty.title')}</Typography>
<Typography level='body-sm' sx={{ color: 'text.secondary' }}> <Typography level='body-sm' sx={{ color: 'text.secondary' }}>
You can still filter the task list with this search. {t('search.empty.subtitle')}
</Typography> </Typography>
</Box> </Box>
)} )}
@@ -381,8 +375,8 @@ const GlobalSearchPalette = ({
}} }}
> >
{!query.trim() && result.provider !== 'actions' {!query.trim() && result.provider !== 'actions'
? 'Recent' ? t('search.recent')
: GROUP_LABELS[result.provider]} : t(`search.groups.${result.provider}`)}
</Typography> </Typography>
)} )}
<ListItemButton <ListItemButton
@@ -434,12 +428,16 @@ const GlobalSearchPalette = ({
color: 'text.tertiary', color: 'text.tertiary',
}} }}
> >
<Typography level='body-xs'> Navigate</Typography> <Typography level='body-xs'>
<Typography level='body-xs'> Open</Typography> {t('search.footer.navigate')}
</Typography>
<Typography level='body-xs'> {t('search.footer.open')}</Typography>
<Typography level='body-xs' sx={{ ml: 'auto' }}> <Typography level='body-xs' sx={{ ml: 'auto' }}>
{query.trim() {query.trim()
? `${Math.max(0, results.length - 1)} results` ? t('search.footer.results', {
: 'Type to search'} count: Math.max(0, results.length - 1),
})
: t('search.footer.typeToSearch')}
</Typography> </Typography>
</Box> </Box>
</SearchContainer> </SearchContainer>

View File

@@ -31,7 +31,6 @@ import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { version } from '../../../package.json' import { version } from '../../../package.json'
import UserProfileAvatar from '../../components/UserProfileAvatar' import UserProfileAvatar from '../../components/UserProfileAvatar'
import Z_INDEX from '../../constants/zIndex' import Z_INDEX from '../../constants/zIndex'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useResource } from '../../queries/ResourceQueries' import { useResource } from '../../queries/ResourceQueries'
import { useGlobalSearch } from '../../search/GlobalSearchContext' import { useGlobalSearch } from '../../search/GlobalSearchContext'
import { apiClient } from '../../utils/ApiClient' import { apiClient } from '../../utils/ApiClient'
@@ -41,7 +40,6 @@ import SyncStatusIndicator from './SyncStatusIndicator'
const publicPages = ['/landing', '/privacy', '/terms'] const publicPages = ['/landing', '/privacy', '/terms']
const NavBar = () => { const NavBar = () => {
const { t } = useTranslation('common') const { t } = useTranslation('common')
const { isRTL } = useLocalization()
const { data: resource } = useResource() const { data: resource } = useResource()
const { openSearch } = useGlobalSearch() const { openSearch } = useGlobalSearch()
@@ -124,7 +122,7 @@ const NavBar = () => {
aria-label='Back from search' aria-label='Back from search'
title={t('back')} title={t('back')}
> >
<ArrowBack /> <ArrowBack className='rtl-flip' />
</IconButton> </IconButton>
) )
} }
@@ -156,7 +154,7 @@ const NavBar = () => {
: t('back') : t('back')
} }
> >
<ArrowBack /> <ArrowBack className='rtl-flip' />
</IconButton> </IconButton>
) )
} }
@@ -224,14 +222,19 @@ const NavBar = () => {
<Drawer <Drawer
open={drawerOpen} open={drawerOpen}
onClose={closeDrawer} onClose={closeDrawer}
anchor={isRTL ? 'right' : 'left'} // Always 'left'. Joy bakes the anchor into emotion CSS (`left: 0` plus a
// translateX for the slide), so stylis-plugin-rtl already mirrors it to
// the right edge under RTL. Branching on isRTL here would flip it twice
// and land the drawer back on the left, half off-screen.
anchor='left'
size='sm' size='sm'
onClick={closeDrawer} onClick={closeDrawer}
sx={{ sx={{
'& .MuiDrawer-content': { '& .MuiDrawer-content': {
position: 'fixed', position: 'fixed',
// pt: 'calc(var(--safe-area-inset-top, 0px))', // pt: 'calc(var(--safe-area-inset-top, 0px))',
...(isRTL ? { right: 0 } : { left: 0 }), // Physical on purpose, so it is mirrored in step with the anchor.
left: 0,
// pb: 'calc(var(--safe-area-inset-bottom, 0px))', // pb: 'calc(var(--safe-area-inset-bottom, 0px))',
// height: // height:
// 'calc(100vh - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))', // 'calc(100vh - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))',