Merge pull request #205 from donetick/localization-rtl
Localization rtl
This commit is contained in:
42
package-lock.json
generated
42
package-lock.json
generated
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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))',
|
||||||
|
|||||||
Reference in New Issue
Block a user