feat: Preferences view layout on mobile has been updated, and can be dismissed by swiping from the right

This commit is contained in:
Aman Harwara
2023-05-17 19:42:08 +05:30
parent 7b44224314
commit e5c580deab
18 changed files with 114 additions and 79 deletions

View File

@@ -1,3 +1,5 @@
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.4167 6.90759L10.2417 10.7326L14.0667 6.90759L15.2417 8.09093L10.2417 13.0909L5.2417 8.09093L6.4167 6.90759Z" fill="#181818"/> <path
d="M6.4167 6.90759L10.2417 10.7326L14.0667 6.90759L15.2417 8.09093L10.2417 13.0909L5.2417 8.09093L6.4167 6.90759Z"
fill="currentColor" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 222 B

After

Width:  |  Height:  |  Size: 243 B

View File

@@ -6,29 +6,32 @@ import { useModalAnimation } from '../Modal/useModalAnimation'
type Props = { type Props = {
isOpen: boolean isOpen: boolean
children: ReactNode children: ReactNode
animationVariant?: 'horizontal' | 'vertical'
} }
const ModalOverlay = forwardRef(({ isOpen, children, ...props }: Props, ref: ForwardedRef<HTMLDivElement>) => { const ModalOverlay = forwardRef(
const [isMounted, setElement] = useModalAnimation(isOpen) ({ isOpen, children, animationVariant, ...props }: Props, ref: ForwardedRef<HTMLDivElement>) => {
const dialog = useDialogStore({ const [isMounted, setElement] = useModalAnimation(isOpen, animationVariant)
open: isMounted, const dialog = useDialogStore({
}) open: isMounted,
})
if (!isMounted) { if (!isMounted) {
return null return null
} }
return ( return (
<Dialog <Dialog
tabIndex={0} tabIndex={0}
className="fixed top-0 left-0 z-modal h-full w-full" className="fixed top-0 left-0 z-modal h-full w-full"
ref={mergeRefs([setElement, ref])} ref={mergeRefs([setElement, ref])}
store={dialog} store={dialog}
{...props} {...props}
> >
{children} {children}
</Dialog> </Dialog>
) )
}) },
)
export default ModalOverlay export default ModalOverlay

View File

@@ -3,49 +3,86 @@ import { useMediaQuery, MutuallyExclusiveMediaQueryBreakpoints } from '@/Hooks/u
export const IosModalAnimationEasing = 'cubic-bezier(.36,.66,.04,1)' export const IosModalAnimationEasing = 'cubic-bezier(.36,.66,.04,1)'
export const useModalAnimation = (isOpen: boolean) => { const Animations = {
vertical: {
enter: {
keyframes: [
{
transform: 'translateY(100%)',
},
{
transform: 'translateY(0)',
},
],
transformOrigin: 'bottom',
},
exit: {
keyframes: [
{
transform: 'translateY(0)',
},
{
transform: 'translateY(100%)',
},
],
transformOrigin: 'bottom',
},
},
horizontal: {
enter: {
keyframes: [
{
transform: 'translateX(100%)',
},
{
transform: 'translateX(0)',
},
],
transformOrigin: 'right',
},
exit: {
keyframes: [
{
transform: 'translateX(0)',
},
{
transform: 'translateX(100%)',
},
],
transformOrigin: 'right',
},
},
}
export const useModalAnimation = (isOpen: boolean, variant: 'horizontal' | 'vertical' = 'vertical') => {
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm) const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
return useLifecycleAnimation( return useLifecycleAnimation(
{ {
open: isOpen, open: isOpen,
enter: { enter: {
keyframes: [ keyframes: Animations[variant].enter.keyframes,
{
transform: 'translateY(100%)',
},
{
transform: 'translateY(0)',
},
],
options: { options: {
easing: IosModalAnimationEasing, easing: IosModalAnimationEasing,
duration: 250, duration: 250,
fill: 'forwards', fill: 'forwards',
}, },
initialStyle: { initialStyle: {
transformOrigin: 'bottom', transformOrigin: Animations[variant].enter.transformOrigin,
}, },
}, },
enterCallback: (element) => { enterCallback: (element) => {
element.scrollTop = 0 element.scrollTop = 0
}, },
exit: { exit: {
keyframes: [ keyframes: Animations[variant].exit.keyframes,
{
transform: 'translateY(0)',
},
{
transform: 'translateY(100%)',
},
],
options: { options: {
easing: IosModalAnimationEasing, easing: IosModalAnimationEasing,
duration: 250, duration: 250,
fill: 'forwards', fill: 'forwards',
}, },
initialStyle: { initialStyle: {
transformOrigin: 'bottom', transformOrigin: Animations[variant].exit.transformOrigin,
}, },
}, },
}, },

View File

@@ -110,7 +110,7 @@ const Email: FunctionComponent<Props> = ({ application }: Props) => {
<PreferencesSegment> <PreferencesSegment>
<Title>Email</Title> <Title>Email</Title>
<div> <div>
<div className="flex items-center justify-between"> <div className="flex items-start justify-between gap-2 md:items-center">
<div className="flex flex-col"> <div className="flex flex-col">
<Subtitle>Mute sign-in notification emails</Subtitle> <Subtitle>Mute sign-in notification emails</Subtitle>
{isMuteSignInEmailsFeatureAvailable ? ( {isMuteSignInEmailsFeatureAvailable ? (
@@ -132,7 +132,7 @@ const Email: FunctionComponent<Props> = ({ application }: Props) => {
)} )}
</div> </div>
{isLoading ? ( {isLoading ? (
<Spinner className="ml-2 flex-shrink-0" /> <Spinner className="h-5 w-5 flex-shrink-0" />
) : ( ) : (
isMuteSignInEmailsFeatureAvailable && ( isMuteSignInEmailsFeatureAvailable && (
<Switch <Switch
@@ -143,13 +143,13 @@ const Email: FunctionComponent<Props> = ({ application }: Props) => {
)} )}
</div> </div>
<HorizontalSeparator classes="my-4" /> <HorizontalSeparator classes="my-4" />
<div className="flex items-center justify-between"> <div className="flex items-start justify-between gap-2 md:items-center">
<div className="flex flex-col"> <div className="flex flex-col">
<Subtitle>Mute marketing notification emails</Subtitle> <Subtitle>Mute marketing notification emails</Subtitle>
<Text>Disables email notifications with special deals and promotions.</Text> <Text>Disables email notifications with special deals and promotions.</Text>
</div> </div>
{isLoading ? ( {isLoading ? (
<Spinner className="ml-2 flex-shrink-0" /> <Spinner className="h-5 w-5 flex-shrink-0" />
) : ( ) : (
<Switch <Switch
onChange={toggleMuteMarketingEmails} onChange={toggleMuteMarketingEmails}

View File

@@ -116,7 +116,7 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
<PreferencesSegment> <PreferencesSegment>
<Title>Themes</Title> <Title>Themes</Title>
<div className="mt-2"> <div className="mt-2">
<div className="flex items-center justify-between"> <div className="flex justify-between gap-2 md:items-center">
<div className="flex flex-col"> <div className="flex flex-col">
<Subtitle>Use system color scheme</Subtitle> <Subtitle>Use system color scheme</Subtitle>
<Text>Automatically change active theme based on your system settings.</Text> <Text>Automatically change active theme based on your system settings.</Text>

View File

@@ -80,7 +80,7 @@ const EditorDefaults = ({ application }: Props) => {
<PreferencesSegment> <PreferencesSegment>
<Title>Editor appearance</Title> <Title>Editor appearance</Title>
<div className="mt-2"> <div className="mt-2">
<div className="flex items-center justify-between"> <div className="flex justify-between gap-2 md:items-center">
<div className="flex flex-col"> <div className="flex flex-col">
<Subtitle>Monospace Font</Subtitle> <Subtitle>Monospace Font</Subtitle>
<Text>Toggles the font style in plaintext and Super notes</Text> <Text>Toggles the font style in plaintext and Super notes</Text>

View File

@@ -122,7 +122,7 @@ const EmailBackups = ({ application }: Props) => {
<Text>How often to receive backups.</Text> <Text>How often to receive backups.</Text>
<div className="mt-2"> <div className="mt-2">
{isLoading ? ( {isLoading ? (
<Spinner className="h-4 w-4" /> <Spinner className="h-5 w-5 flex-shrink-0" />
) : ( ) : (
<Dropdown <Dropdown
label="Select email frequency" label="Select email frequency"
@@ -134,12 +134,12 @@ const EmailBackups = ({ application }: Props) => {
</div> </div>
<HorizontalSeparator classes="my-4" /> <HorizontalSeparator classes="my-4" />
<Subtitle>Email preferences</Subtitle> <Subtitle>Email preferences</Subtitle>
<div className="flex items-center justify-between"> <div className="flex justify-between gap-2 md:items-center">
<div className="flex flex-col"> <div className="flex flex-col">
<Text>Receive a notification email if an email backup fails.</Text> <Text>Receive a notification email if an email backup fails.</Text>
</div> </div>
{isLoading ? ( {isLoading ? (
<Spinner className="h-4 w-4" /> <Spinner className="h-5 w-5 flex-shrink-0" />
) : ( ) : (
<Switch onChange={toggleMuteFailedBackupEmails} checked={!isFailedBackupEmailMuted} /> <Switch onChange={toggleMuteFailedBackupEmails} checked={!isFailedBackupEmailMuted} />
)} )}

View File

@@ -44,7 +44,7 @@ const Defaults: FunctionComponent<Props> = ({ application }) => {
<Title>Defaults</Title> <Title>Defaults</Title>
{application.platform === Platform.Android && ( {application.platform === Platform.Android && (
<> <>
<div className="flex items-center justify-between"> <div className="flex justify-between gap-2 md:items-center">
<div className="flex flex-col"> <div className="flex flex-col">
<Subtitle>Always ask before closing app (Android)</Subtitle> <Subtitle>Always ask before closing app (Android)</Subtitle>
<Text>Whether a confirmation dialog should be shown before closing the app.</Text> <Text>Whether a confirmation dialog should be shown before closing the app.</Text>
@@ -54,7 +54,7 @@ const Defaults: FunctionComponent<Props> = ({ application }) => {
<HorizontalSeparator classes="my-4" /> <HorizontalSeparator classes="my-4" />
</> </>
)} )}
<div className="flex items-center justify-between"> <div className="flex justify-between gap-2 md:items-center">
<div className="flex flex-col"> <div className="flex flex-col">
<Subtitle>Spellcheck</Subtitle> <Subtitle>Spellcheck</Subtitle>
<Text> <Text>
@@ -65,7 +65,7 @@ const Defaults: FunctionComponent<Props> = ({ application }) => {
<Switch onChange={toggleSpellcheck} checked={spellcheck} /> <Switch onChange={toggleSpellcheck} checked={spellcheck} />
</div> </div>
<HorizontalSeparator classes="my-4" /> <HorizontalSeparator classes="my-4" />
<div className="flex items-center justify-between"> <div className="flex justify-between gap-2 md:items-center">
<div className="flex flex-col"> <div className="flex flex-col">
<Subtitle>Add all parent tags when adding a nested tag to a note</Subtitle> <Subtitle>Add all parent tags when adding a nested tag to a note</Subtitle>
<Text>When enabled, adding a nested tag to a note will automatically add all associated parent tags.</Text> <Text>When enabled, adding a nested tag to a note will automatically add all associated parent tags.</Text>

View File

@@ -62,13 +62,14 @@ const LabsPane: FunctionComponent<Props> = ({ application }) => {
const premiumModal = usePremiumModal() const premiumModal = usePremiumModal()
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm) const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
const canShowPaneGesturesOption = isMobileScreen && typeof isPaneGesturesEnabled === 'boolean'
return ( return (
<PreferencesGroup> <PreferencesGroup>
<PreferencesSegment> <PreferencesSegment>
<Title>Labs</Title> <Title>Labs</Title>
<div> <div>
{isMobileScreen && ( {canShowPaneGesturesOption && (
<LabsFeature <LabsFeature
name="Pane switch gestures" name="Pane switch gestures"
description="Allows using gestures to navigate" description="Allows using gestures to navigate"
@@ -103,7 +104,7 @@ const LabsPane: FunctionComponent<Props> = ({ application }) => {
</Fragment> </Fragment>
) )
})} })}
{(experimentalFeatures.length === 0 || typeof isPaneGesturesEnabled === 'boolean') && ( {experimentalFeatures.length === 0 && !canShowPaneGesturesOption && (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex flex-col"> <div className="flex flex-col">
<Text>No experimental features available.</Text> <Text>No experimental features available.</Text>

View File

@@ -10,7 +10,7 @@ type Props = {
const LabsFeature = ({ name, description, toggleFeature, isEnabled }: Props) => { const LabsFeature = ({ name, description, toggleFeature, isEnabled }: Props) => {
return ( return (
<div className="flex items-center justify-between"> <div className="flex justify-between gap-2 md:items-center">
<div className="flex flex-col"> <div className="flex flex-col">
<Subtitle>{name}</Subtitle> <Subtitle>{name}</Subtitle>
<Text>{description}</Text> <Text>{description}</Text>

View File

@@ -27,7 +27,7 @@ const Tools: FunctionComponent<Props> = ({ application }: Props) => {
<PreferencesSegment> <PreferencesSegment>
<Title>Tools</Title> <Title>Tools</Title>
<div> <div>
<div className="flex items-center justify-between"> <div className="flex justify-between gap-2 md:items-center">
<div className="flex flex-col"> <div className="flex flex-col">
<Subtitle>Show note saving status while editing</Subtitle> <Subtitle>Show note saving status while editing</Subtitle>
<Text> <Text>

View File

@@ -77,7 +77,7 @@ const Privacy: FunctionComponent<Props> = ({ application }: Props) => {
<PreferencesSegment> <PreferencesSegment>
<Title>Privacy</Title> <Title>Privacy</Title>
<div> <div>
<div className="flex items-center justify-between"> <div className="flex justify-between gap-2 md:items-center">
<div className="flex flex-col"> <div className="flex flex-col">
<Subtitle>Session user agent logging</Subtitle> <Subtitle>Session user agent logging</Subtitle>
<Text> <Text>
@@ -87,7 +87,7 @@ const Privacy: FunctionComponent<Props> = ({ application }: Props) => {
</Text> </Text>
</div> </div>
{isLoading ? ( {isLoading ? (
<Spinner className="ml-2 flex-shrink-0" /> <Spinner className="h-5 w-5 flex-shrink-0" />
) : ( ) : (
<Switch <Switch
onChange={toggleSessionLogging} onChange={toggleSessionLogging}

View File

@@ -91,14 +91,12 @@ const TwoFactorAuthView: FunctionComponent<Props> = ({ auth, application }) => {
<> <>
<PreferencesGroup> <PreferencesGroup>
<PreferencesSegment> <PreferencesSegment>
<div className="flex flex-row items-center"> <div className="flex flex-row gap-2 md:items-center">
<div className="flex flex-grow flex-col"> <div className="flex flex-grow flex-col">
<TwoFactorTitle auth={auth} /> <TwoFactorTitle auth={auth} />
<TwoFactorDescription auth={auth} /> <TwoFactorDescription auth={auth} />
</div> </div>
<div className="flex min-w-15 flex-col items-center justify-center"> <TwoFactorSwitch auth={auth} />
<TwoFactorSwitch auth={auth} />
</div>
</div> </div>
</PreferencesSegment> </PreferencesSegment>

View File

@@ -6,7 +6,7 @@ import PaneSelector from './PaneSelector'
import { PreferencesProps } from './PreferencesProps' import { PreferencesProps } from './PreferencesProps'
const PreferencesCanvas: FunctionComponent<PreferencesProps & { menu: PreferencesMenu }> = (props) => ( const PreferencesCanvas: FunctionComponent<PreferencesProps & { menu: PreferencesMenu }> = (props) => (
<div className="flex min-h-0 flex-grow flex-col-reverse md:flex-row md:justify-between"> <div className="flex min-h-0 flex-grow flex-col md:flex-row md:justify-between">
<PreferencesMenuView menu={props.menu} /> <PreferencesMenuView menu={props.menu} />
<div className="min-h-0 flex-grow overflow-auto bg-contrast"> <div className="min-h-0 flex-grow overflow-auto bg-contrast">
<PaneSelector {...props} /> <PaneSelector {...props} />

View File

@@ -30,7 +30,7 @@ const PreferencesMenuView: FunctionComponent<Props> = ({ menu }) => {
return ( return (
<div <div
className={classNames( className={classNames(
'border-t border-border bg-default px-5 pt-2 md:border-0 md:bg-contrast md:px-0 md:py-0', 'border-b border-border bg-default px-5 pt-2 md:border-0 md:bg-contrast md:px-0 md:py-0',
hasBottomInset ? 'pb-safe-bottom' : 'pb-2 md:pb-0', hasBottomInset ? 'pb-safe-bottom' : 'pb-2 md:pb-0',
)} )}
> >
@@ -60,7 +60,7 @@ const PreferencesMenuView: FunctionComponent<Props> = ({ menu }) => {
wrapper: 'relative', wrapper: 'relative',
button: 'focus:outline-none focus:shadow-none focus:ring-none', button: 'focus:outline-none focus:shadow-none focus:ring-none',
}} }}
popoverPlacement="top" popoverPlacement="bottom"
/> />
</div> </div>
</div> </div>

View File

@@ -47,7 +47,11 @@ const PreferencesViewWrapper: FunctionComponent<PreferencesViewWrapperProps> = (
}) })
return ( return (
<ModalOverlay isOpen={viewControllerManager.preferencesController.isOpen} ref={setElement}> <ModalOverlay
isOpen={viewControllerManager.preferencesController.isOpen}
ref={setElement}
animationVariant="horizontal"
>
<PreferencesView <PreferencesView
closePreferences={viewControllerManager.preferencesController.closePreferences} closePreferences={viewControllerManager.preferencesController.closePreferences}
application={application} application={application}

View File

@@ -1,6 +1,7 @@
import { FunctionComponent, ReactNode, useRef, useState } from 'react' import { FunctionComponent, ReactNode, useRef, useState } from 'react'
import { ArrowDownCheckmarkIcon } from '@standardnotes/icons' import { ArrowDownCheckmarkIcon } from '@standardnotes/icons'
import { Title } from '@/Components/Preferences/PreferencesComponents/Content' import { Title } from '@/Components/Preferences/PreferencesComponents/Content'
import { classNames } from '@standardnotes/snjs'
type Props = { type Props = {
title: string | JSX.Element title: string | JSX.Element
@@ -21,12 +22,7 @@ const AccordionItem: FunctionComponent<Props> = ({ title, className = '', childr
}} }}
> >
<Title>{title}</Title> <Title>{title}</Title>
<ArrowDownCheckmarkIcon <ArrowDownCheckmarkIcon className={classNames('h-5 w-5 text-info', isExpanded && 'rotate-180')} />
className="sn-accordion-arrow-icon"
width={20}
height={20}
data-is-expanded={isExpanded}
/>
</div> </div>
<div className={'accordion-contents-container cursor-auto'} data-is-expanded={isExpanded} ref={elementRef}> <div className={'accordion-contents-container cursor-auto'} data-is-expanded={isExpanded} ref={elementRef}>
{children} {children}

View File

@@ -16,12 +16,6 @@
} }
} }
.sn-accordion-arrow-icon {
&[data-is-expanded='true'] {
transform: rotate(180deg);
}
}
.accordion-contents-container { .accordion-contents-container {
transition: all 0.23s ease-out; transition: all 0.23s ease-out;
transform-origin: top; transform-origin: top;