mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 12:43:09 -05:00
Improve sidebar auto-floating behavior (#868)
I got some feedback in the discord that this behavior was disruptive when zooming in. - I’ve reduced the breakpoint so hopefully the disruption of the transition is matched by a significant improvement in available space now. - Also the 2 places in the app that use window width (including here) now check for the width of the `<html>` tag, not the width of the viewport (those 2 values can be different when doing a pinch-zoom, causing undesirable layout shifts.) - Most of the logic has been rewritten to improve the transitions Mobile & desktop experience https://user-images.githubusercontent.com/25517624/233653721-b13c5e22-ae11-4bdf-a494-a6916556d05e.mov https://user-images.githubusercontent.com/25517624/233654784-b6cc1006-44ea-4066-be7a-8d0dd786fb5b.mov (I’d like tapping on something to close the sidebar on mobile, but that can be approached in a future PR)
This commit is contained in:
@@ -28,6 +28,8 @@ module.exports = {
|
||||
require('confusing-browser-globals').filter(g => g !== 'self'),
|
||||
),
|
||||
|
||||
'react/jsx-no-useless-fragment': 'error',
|
||||
|
||||
'rulesdir/typography': 'error',
|
||||
|
||||
// https://github.com/eslint/eslint/issues/16954
|
||||
|
||||
@@ -234,10 +234,7 @@ class FinancesApp extends React.Component {
|
||||
}
|
||||
|
||||
handleWindowResize() {
|
||||
this.setState({
|
||||
isMobile: isMobile(),
|
||||
windowWidth: window.innerWidth,
|
||||
});
|
||||
this.setState({ isMobile: isMobile() });
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import React, { useState, useContext, useMemo } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import { useViewportSize } from '@react-aria/utils';
|
||||
import mitt from 'mitt';
|
||||
|
||||
import * as actions from 'loot-core/src/client/actions';
|
||||
|
||||
import { colors } from '../style';
|
||||
import { breakpoints } from '../tokens';
|
||||
|
||||
import { View } from './common';
|
||||
import { SIDEBAR_WIDTH } from './sidebar';
|
||||
import SidebarWithData from './SidebarWithData';
|
||||
@@ -17,115 +13,66 @@ import SidebarWithData from './SidebarWithData';
|
||||
const SidebarContext = React.createContext(null);
|
||||
|
||||
export function SidebarProvider({ children }) {
|
||||
let emitter = mitt();
|
||||
let [hidden, setHidden] = useState(true);
|
||||
return (
|
||||
<SidebarContext.Provider
|
||||
value={{
|
||||
show: () => emitter.emit('show'),
|
||||
hide: () => emitter.emit('hide'),
|
||||
toggle: () => emitter.emit('toggle'),
|
||||
on: (name, listener) => {
|
||||
emitter.on(name, listener);
|
||||
return () => emitter.off(name, listener);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SidebarContext.Provider value={[hidden, setHidden]}>
|
||||
{children}
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSidebar() {
|
||||
return useContext(SidebarContext);
|
||||
useViewportSize(); // Force re-render on window resize
|
||||
let windowWidth = document.documentElement.clientWidth;
|
||||
let alwaysFloats = windowWidth < 668;
|
||||
|
||||
let [hidden, setHidden] = useContext(SidebarContext);
|
||||
return useMemo(
|
||||
() => ({ hidden, setHidden, alwaysFloats }),
|
||||
[hidden, setHidden, alwaysFloats],
|
||||
);
|
||||
}
|
||||
|
||||
function Sidebar({ floatingSidebar }) {
|
||||
let [hidden, setHidden] = useState(true);
|
||||
let sidebar = useSidebar();
|
||||
|
||||
let windowWidth = useViewportSize().width;
|
||||
let sidebarShouldFloat = floatingSidebar || windowWidth < breakpoints.medium;
|
||||
|
||||
if (!sidebarShouldFloat && hidden) {
|
||||
setHidden(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let cleanups = [
|
||||
sidebar.on('show', () => setHidden(false)),
|
||||
sidebar.on('hide', () => setHidden(true)),
|
||||
sidebar.on('toggle', () => setHidden(hidden => !hidden)),
|
||||
];
|
||||
return () => {
|
||||
cleanups.forEach(fn => fn());
|
||||
};
|
||||
}, [sidebar]);
|
||||
let sidebarShouldFloat = floatingSidebar || sidebar.alwaysFloats;
|
||||
|
||||
return (
|
||||
<>
|
||||
{sidebarShouldFloat && (
|
||||
<View
|
||||
onMouseOver={() => setHidden(false)}
|
||||
onMouseLeave={() => setHidden(true)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: hidden ? 0 : 160,
|
||||
zIndex: 999,
|
||||
}}
|
||||
></View>
|
||||
)}
|
||||
|
||||
<View
|
||||
onMouseOver={
|
||||
sidebarShouldFloat
|
||||
? e => {
|
||||
e.stopPropagation();
|
||||
setHidden(false);
|
||||
}
|
||||
: null
|
||||
}
|
||||
onMouseLeave={sidebarShouldFloat ? () => setHidden(true) : null}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 50,
|
||||
// If not floating, the -50 takes into account the transform below
|
||||
bottom: sidebarShouldFloat ? 50 : -50,
|
||||
zIndex: 1001,
|
||||
borderRadius: '0 6px 6px 0',
|
||||
overflow: 'hidden',
|
||||
boxShadow:
|
||||
!sidebarShouldFloat || hidden
|
||||
? 'none'
|
||||
: '0 15px 30px 0 rgba(0,0,0,0.25), 0 3px 15px 0 rgba(0,0,0,.5)',
|
||||
transform: `translateY(${!sidebarShouldFloat ? -50 : 0}px)
|
||||
translateX(${hidden ? -SIDEBAR_WIDTH : 0}px)`,
|
||||
transition: 'transform .5s, box-shadow .5s',
|
||||
}}
|
||||
>
|
||||
<SidebarWithData />
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
backgroundColor: colors.n1,
|
||||
opacity: sidebarShouldFloat ? 0 : 1,
|
||||
transform: `translateX(${sidebarShouldFloat ? -50 : 0}px)`,
|
||||
transition: 'transform .4s, opacity .2s',
|
||||
width: SIDEBAR_WIDTH,
|
||||
},
|
||||
sidebarShouldFloat && {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
]}
|
||||
></View>
|
||||
</>
|
||||
<View
|
||||
onMouseOver={
|
||||
sidebarShouldFloat
|
||||
? e => {
|
||||
e.stopPropagation();
|
||||
sidebar.setHidden(false);
|
||||
}
|
||||
: null
|
||||
}
|
||||
onMouseLeave={sidebarShouldFloat ? () => sidebar.setHidden(true) : null}
|
||||
style={{
|
||||
position: sidebarShouldFloat ? 'absolute' : null,
|
||||
top: 12,
|
||||
// If not floating, the -50 takes into account the transform below
|
||||
bottom: sidebarShouldFloat ? 12 : -50,
|
||||
zIndex: 1001,
|
||||
borderRadius: sidebarShouldFloat ? '0 6px 6px 0' : 0,
|
||||
overflow: 'hidden',
|
||||
boxShadow:
|
||||
!sidebarShouldFloat || sidebar.hidden
|
||||
? 'none'
|
||||
: '0 15px 30px 0 rgba(0,0,0,0.25), 0 3px 15px 0 rgba(0,0,0,.5)',
|
||||
transform: `translateY(${!sidebarShouldFloat ? -12 : 0}px)
|
||||
translateX(${
|
||||
sidebarShouldFloat && sidebar.hidden
|
||||
? -SIDEBAR_WIDTH
|
||||
: 0
|
||||
}px)`,
|
||||
transition:
|
||||
'transform .5s, box-shadow .5s, border-radius .5s, bottom .5s',
|
||||
}}
|
||||
>
|
||||
<SidebarWithData />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useState, useEffect, useRef, useContext } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Switch, Route, withRouter } from 'react-router-dom';
|
||||
|
||||
import { useViewportSize } from '@react-aria/utils';
|
||||
import { css, media } from 'glamor';
|
||||
|
||||
import * as actions from 'loot-core/src/client/actions';
|
||||
@@ -13,10 +12,9 @@ import { listen } from 'loot-core/src/platform/client/fetch';
|
||||
import useFeatureFlag from '../hooks/useFeatureFlag';
|
||||
import ArrowLeft from '../icons/v1/ArrowLeft';
|
||||
import AlertTriangle from '../icons/v2/AlertTriangle';
|
||||
import ArrowButtonRight1 from '../icons/v2/ArrowButtonRight1';
|
||||
import NavigationMenu from '../icons/v2/NavigationMenu';
|
||||
import { colors } from '../style';
|
||||
import tokens, { breakpoints } from '../tokens';
|
||||
import tokens from '../tokens';
|
||||
|
||||
import AccountSyncCheck from './accounts/AccountSyncCheck';
|
||||
import AnimatedRefresh from './AnimatedRefresh';
|
||||
@@ -267,9 +265,6 @@ function Titlebar({
|
||||
let sidebar = useSidebar();
|
||||
const serverURL = useServerURL();
|
||||
|
||||
let windowWidth = useViewportSize().width;
|
||||
let sidebarAlwaysFloats = windowWidth < breakpoints.medium;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -289,50 +284,30 @@ function Titlebar({
|
||||
style,
|
||||
]}
|
||||
>
|
||||
{(floatingSidebar || sidebarAlwaysFloats) && (
|
||||
{(floatingSidebar || sidebar.alwaysFloats) && (
|
||||
<Button
|
||||
bare
|
||||
style={{
|
||||
marginRight: 8,
|
||||
'& .arrow-right': { opacity: 0, transition: 'opacity .3s' },
|
||||
'& .menu': { opacity: 1, transition: 'opacity .3s' },
|
||||
'&:hover .arrow-right': !sidebarAlwaysFloats && { opacity: 1 },
|
||||
'&:hover .menu': !sidebarAlwaysFloats && { opacity: 0 },
|
||||
style={{ marginRight: 8 }}
|
||||
onPointerEnter={e => {
|
||||
if (e.pointerType === 'mouse') {
|
||||
sidebar.setHidden(false);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => sidebar.show()}
|
||||
onMouseLeave={() => sidebar.hide()}
|
||||
onClick={() => {
|
||||
if (windowWidth >= breakpoints.medium) {
|
||||
saveGlobalPrefs({ floatingSidebar: !floatingSidebar });
|
||||
} else {
|
||||
sidebar.toggle();
|
||||
onPointerLeave={e => {
|
||||
if (e.pointerType === 'mouse') {
|
||||
sidebar.setHidden(true);
|
||||
}
|
||||
}}
|
||||
onPointerUp={e => {
|
||||
if (e.pointerType !== 'mouse') {
|
||||
sidebar.setHidden(!sidebar.hidden);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View style={{ width: 15, height: 15 }}>
|
||||
<ArrowButtonRight1
|
||||
className="arrow-right"
|
||||
style={{
|
||||
width: 13,
|
||||
height: 13,
|
||||
color: colors.n5,
|
||||
position: 'absolute',
|
||||
top: 1,
|
||||
left: 1,
|
||||
}}
|
||||
/>
|
||||
<NavigationMenu
|
||||
className="menu"
|
||||
style={{
|
||||
width: 15,
|
||||
height: 15,
|
||||
color: colors.n5,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<NavigationMenu
|
||||
className="menu"
|
||||
style={{ width: 15, height: 15, color: colors.n5, left: 0 }}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
@@ -394,48 +394,46 @@ function ListBoxSection({ section, state }) {
|
||||
// The heading is rendered inside an <li> element, which contains
|
||||
// a <ul> with the child items.
|
||||
return (
|
||||
<>
|
||||
<li {...itemProps} style={{ width: '100%' }}>
|
||||
{section.rendered && (
|
||||
<div
|
||||
{...headingProps}
|
||||
style={{
|
||||
...styles.smallText,
|
||||
backgroundColor: colors.n10,
|
||||
borderBottom: `1px solid ${colors.n9}`,
|
||||
borderTop: `1px solid ${colors.n9}`,
|
||||
color: colors.n4,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
paddingBottom: 4,
|
||||
paddingTop: 4,
|
||||
position: 'sticky',
|
||||
top: '0',
|
||||
width: '100%',
|
||||
zIndex: zIndices.SECTION_HEADING,
|
||||
}}
|
||||
>
|
||||
{section.rendered}
|
||||
</div>
|
||||
)}
|
||||
<ul
|
||||
{...groupProps}
|
||||
<li {...itemProps} style={{ width: '100%' }}>
|
||||
{section.rendered && (
|
||||
<div
|
||||
{...headingProps}
|
||||
style={{
|
||||
padding: 0,
|
||||
listStyle: 'none',
|
||||
...styles.smallText,
|
||||
backgroundColor: colors.n10,
|
||||
borderBottom: `1px solid ${colors.n9}`,
|
||||
borderTop: `1px solid ${colors.n9}`,
|
||||
color: colors.n4,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
paddingBottom: 4,
|
||||
paddingTop: 4,
|
||||
position: 'sticky',
|
||||
top: '0',
|
||||
width: '100%',
|
||||
zIndex: zIndices.SECTION_HEADING,
|
||||
}}
|
||||
>
|
||||
{[...section.childNodes].map((node, index, nodes) => (
|
||||
<Option
|
||||
key={node.key}
|
||||
item={node}
|
||||
state={state}
|
||||
isLast={index === nodes.length - 1}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
</>
|
||||
{section.rendered}
|
||||
</div>
|
||||
)}
|
||||
<ul
|
||||
{...groupProps}
|
||||
style={{
|
||||
padding: 0,
|
||||
listStyle: 'none',
|
||||
}}
|
||||
>
|
||||
{[...section.childNodes].map((node, index, nodes) => (
|
||||
<Option
|
||||
key={node.key}
|
||||
item={node}
|
||||
state={state}
|
||||
isLast={index === nodes.length - 1}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -424,28 +424,26 @@ function PayeeCell({
|
||||
inputStyle,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<PayeeAutocomplete
|
||||
payees={payees}
|
||||
accounts={accounts}
|
||||
value={payeeId}
|
||||
shouldSaveFromKey={shouldSaveFromKey}
|
||||
inputProps={{
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
style: inputStyle,
|
||||
}}
|
||||
showManagePayees={true}
|
||||
tableBehavior={true}
|
||||
defaultFocusTransferPayees={transaction.is_child}
|
||||
focused={true}
|
||||
onUpdate={onUpdate}
|
||||
onSelect={onSave}
|
||||
onManagePayees={() => onManagePayees(payeeId)}
|
||||
isCreatable
|
||||
menuPortalTarget={undefined}
|
||||
/>
|
||||
</>
|
||||
<PayeeAutocomplete
|
||||
payees={payees}
|
||||
accounts={accounts}
|
||||
value={payeeId}
|
||||
shouldSaveFromKey={shouldSaveFromKey}
|
||||
inputProps={{
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
style: inputStyle,
|
||||
}}
|
||||
showManagePayees={true}
|
||||
tableBehavior={true}
|
||||
defaultFocusTransferPayees={transaction.is_child}
|
||||
focused={true}
|
||||
onUpdate={onUpdate}
|
||||
onSelect={onSave}
|
||||
onManagePayees={() => onManagePayees(payeeId)}
|
||||
isCreatable
|
||||
menuPortalTarget={undefined}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</CustomCell>
|
||||
|
||||
@@ -85,119 +85,117 @@ export default function ConfigServer() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={{ maxWidth: 500, marginTop: -30 }}>
|
||||
<Title text="Where’s the server?" />
|
||||
<View style={{ maxWidth: 500, marginTop: -30 }}>
|
||||
<Title text="Where’s the server?" />
|
||||
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: colors.n2,
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{currentUrl ? (
|
||||
<>
|
||||
Existing sessions will be logged out and you will log in to this
|
||||
server. We will validate that Actual is running at this URL.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
There is no server configured. After running the server, specify the
|
||||
URL here to use the app. You can always change this later. We will
|
||||
validate that Actual is running at this URL.
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{error && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: colors.n2,
|
||||
lineHeight: 1.5,
|
||||
marginTop: 20,
|
||||
color: colors.r4,
|
||||
borderRadius: 4,
|
||||
fontSize: 15,
|
||||
}}
|
||||
>
|
||||
{currentUrl ? (
|
||||
<>
|
||||
Existing sessions will be logged out and you will log in to this
|
||||
server. We will validate that Actual is running at this URL.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
There is no server configured. After running the server, specify
|
||||
the URL here to use the app. You can always change this later. We
|
||||
will validate that Actual is running at this URL.
|
||||
</>
|
||||
)}
|
||||
{getErrorMessage(error)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Text
|
||||
style={{
|
||||
marginTop: 20,
|
||||
color: colors.r4,
|
||||
borderRadius: 4,
|
||||
fontSize: 15,
|
||||
}}
|
||||
<form
|
||||
style={{ display: 'flex', flexDirection: 'row', marginTop: 30 }}
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
onSubmit();
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
autoFocus={true}
|
||||
placeholder={'https://example.com'}
|
||||
value={url}
|
||||
onChange={e => setUrl(e.target.value)}
|
||||
style={{ flex: 1, marginRight: 10 }}
|
||||
/>
|
||||
<ButtonWithLoading primary loading={loading} style={{ fontSize: 15 }}>
|
||||
OK
|
||||
</ButtonWithLoading>
|
||||
{currentUrl && (
|
||||
<Button
|
||||
bare
|
||||
type="button"
|
||||
style={{ fontSize: 15, marginLeft: 10 }}
|
||||
onClick={() => history.goBack()}
|
||||
>
|
||||
{getErrorMessage(error)}
|
||||
</Text>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<form
|
||||
style={{ display: 'flex', flexDirection: 'row', marginTop: 30 }}
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
onSubmit();
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
autoFocus={true}
|
||||
placeholder={'https://example.com'}
|
||||
value={url}
|
||||
onChange={e => setUrl(e.target.value)}
|
||||
style={{ flex: 1, marginRight: 10 }}
|
||||
/>
|
||||
<ButtonWithLoading primary loading={loading} style={{ fontSize: 15 }}>
|
||||
OK
|
||||
</ButtonWithLoading>
|
||||
{currentUrl && (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
flexFlow: 'row wrap',
|
||||
justifyContent: 'center',
|
||||
marginTop: 15,
|
||||
}}
|
||||
>
|
||||
{currentUrl ? (
|
||||
<Button bare style={{ color: colors.n4 }} onClick={onSkip}>
|
||||
Stop using a server
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
bare
|
||||
type="button"
|
||||
style={{ fontSize: 15, marginLeft: 10 }}
|
||||
onClick={() => history.goBack()}
|
||||
style={{
|
||||
color: colors.n4,
|
||||
margin: 5,
|
||||
marginRight: 15,
|
||||
}}
|
||||
onClick={onSameDomain}
|
||||
>
|
||||
Cancel
|
||||
Use {window.location.origin.replace(/https?:\/\//, '')}
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
flexFlow: 'row wrap',
|
||||
justifyContent: 'center',
|
||||
marginTop: 15,
|
||||
}}
|
||||
>
|
||||
{currentUrl ? (
|
||||
<Button bare style={{ color: colors.n4 }} onClick={onSkip}>
|
||||
Stop using a server
|
||||
<Button
|
||||
bare
|
||||
style={{ color: colors.n4, margin: 5 }}
|
||||
onClick={onSkip}
|
||||
>
|
||||
Don’t use a server
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
bare
|
||||
style={{
|
||||
color: colors.n4,
|
||||
margin: 5,
|
||||
marginRight: 15,
|
||||
}}
|
||||
onClick={onSameDomain}
|
||||
>
|
||||
Use {window.location.origin.replace(/https?:\/\//, '')}
|
||||
</Button>
|
||||
<Button
|
||||
bare
|
||||
style={{ color: colors.n4, margin: 5 }}
|
||||
onClick={onSkip}
|
||||
>
|
||||
Don’t use a server
|
||||
</Button>
|
||||
|
||||
{isNonProductionEnvironment() && (
|
||||
<Button
|
||||
primary
|
||||
style={{ marginLeft: 15 }}
|
||||
onClick={onCreateTestFile}
|
||||
>
|
||||
Create test file
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
{isNonProductionEnvironment() && (
|
||||
<Button
|
||||
primary
|
||||
style={{ marginLeft: 15 }}
|
||||
onClick={onCreateTestFile}
|
||||
>
|
||||
Create test file
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,55 +50,53 @@ export default function Bootstrap() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={{ maxWidth: 450, marginTop: -30 }}>
|
||||
<Title text="Welcome to Actual!" />
|
||||
<P style={{ fontSize: 16, color: colors.n2 }}>
|
||||
Actual is a super fast privacy-focused app for managing your finances.
|
||||
To secure your data, you’ll need to set a password for your server.
|
||||
</P>
|
||||
<View style={{ maxWidth: 450, marginTop: -30 }}>
|
||||
<Title text="Welcome to Actual!" />
|
||||
<P style={{ fontSize: 16, color: colors.n2 }}>
|
||||
Actual is a super fast privacy-focused app for managing your finances.
|
||||
To secure your data, you’ll need to set a password for your server.
|
||||
</P>
|
||||
|
||||
<P isLast style={{ fontSize: 16, color: colors.n2 }}>
|
||||
Consider opening{' '}
|
||||
<a
|
||||
href="https://actualbudget.github.io/docs/Getting-Started/using-actual/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: colors.b4 }}
|
||||
<P isLast style={{ fontSize: 16, color: colors.n2 }}>
|
||||
Consider opening{' '}
|
||||
<a
|
||||
href="https://actualbudget.github.io/docs/Getting-Started/using-actual/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: colors.b4 }}
|
||||
>
|
||||
our tour
|
||||
</a>{' '}
|
||||
in a new tab for some guidance on what to do when you’ve set your
|
||||
password.
|
||||
</P>
|
||||
|
||||
{error && (
|
||||
<Text
|
||||
style={{
|
||||
marginTop: 20,
|
||||
color: colors.r4,
|
||||
borderRadius: 4,
|
||||
fontSize: 15,
|
||||
}}
|
||||
>
|
||||
{getErrorMessage(error)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<ConfirmPasswordForm
|
||||
buttons={
|
||||
<Button
|
||||
bare
|
||||
style={{ fontSize: 15, color: colors.b4, marginRight: 15 }}
|
||||
onClick={onDemo}
|
||||
>
|
||||
our tour
|
||||
</a>{' '}
|
||||
in a new tab for some guidance on what to do when you’ve set your
|
||||
password.
|
||||
</P>
|
||||
|
||||
{error && (
|
||||
<Text
|
||||
style={{
|
||||
marginTop: 20,
|
||||
color: colors.r4,
|
||||
borderRadius: 4,
|
||||
fontSize: 15,
|
||||
}}
|
||||
>
|
||||
{getErrorMessage(error)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<ConfirmPasswordForm
|
||||
buttons={
|
||||
<Button
|
||||
bare
|
||||
style={{ fontSize: 15, color: colors.b4, marginRight: 15 }}
|
||||
onClick={onDemo}
|
||||
>
|
||||
Try Demo
|
||||
</Button>
|
||||
}
|
||||
onSetPassword={onSetPassword}
|
||||
onError={setError}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
Try Demo
|
||||
</Button>
|
||||
}
|
||||
onSetPassword={onSetPassword}
|
||||
onError={setError}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,61 +41,59 @@ export default function ChangePassword() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={{ maxWidth: 500, marginTop: -30 }}>
|
||||
<Title text="Change server password" />
|
||||
<View style={{ maxWidth: 500, marginTop: -30 }}>
|
||||
<Title text="Change server password" />
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: colors.n2,
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
This will change the password for this server instance. All existing
|
||||
sessions will stay logged in.
|
||||
</Text>
|
||||
|
||||
{error && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: colors.n2,
|
||||
lineHeight: 1.4,
|
||||
marginTop: 20,
|
||||
color: colors.r4,
|
||||
borderRadius: 4,
|
||||
fontSize: 15,
|
||||
}}
|
||||
>
|
||||
This will change the password for this server instance. All existing
|
||||
sessions will stay logged in.
|
||||
{getErrorMessage(error)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Text
|
||||
style={{
|
||||
marginTop: 20,
|
||||
color: colors.r4,
|
||||
borderRadius: 4,
|
||||
fontSize: 15,
|
||||
}}
|
||||
{msg && (
|
||||
<Text
|
||||
style={{
|
||||
marginTop: 20,
|
||||
color: colors.g4,
|
||||
borderRadius: 4,
|
||||
fontSize: 15,
|
||||
}}
|
||||
>
|
||||
{msg}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<ConfirmPasswordForm
|
||||
buttons={
|
||||
<Button
|
||||
bare
|
||||
type="button"
|
||||
style={{ fontSize: 15, marginRight: 10 }}
|
||||
onClick={() => history.push('/')}
|
||||
>
|
||||
{getErrorMessage(error)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{msg && (
|
||||
<Text
|
||||
style={{
|
||||
marginTop: 20,
|
||||
color: colors.g4,
|
||||
borderRadius: 4,
|
||||
fontSize: 15,
|
||||
}}
|
||||
>
|
||||
{msg}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<ConfirmPasswordForm
|
||||
buttons={
|
||||
<Button
|
||||
bare
|
||||
type="button"
|
||||
style={{ fontSize: 15, marginRight: 10 }}
|
||||
onClick={() => history.push('/')}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
}
|
||||
onSetPassword={onSetPassword}
|
||||
onError={setError}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
Cancel
|
||||
</Button>
|
||||
}
|
||||
onSetPassword={onSetPassword}
|
||||
onError={setError}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,64 +56,62 @@ export default function Login() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={{ maxWidth: 450, marginTop: -30 }}>
|
||||
<Title text="Sign in to this Actual instance" />
|
||||
<View style={{ maxWidth: 450, marginTop: -30 }}>
|
||||
<Title text="Sign in to this Actual instance" />
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: colors.n2,
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
If you lost your password, you likely still have access to your server
|
||||
to manually reset it.
|
||||
</Text>
|
||||
|
||||
{error && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: colors.n2,
|
||||
lineHeight: 1.4,
|
||||
marginTop: 20,
|
||||
color: colors.r4,
|
||||
borderRadius: 4,
|
||||
fontSize: 15,
|
||||
}}
|
||||
>
|
||||
If you lost your password, you likely still have access to your server
|
||||
to manually reset it.
|
||||
{getErrorMessage(error)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Text
|
||||
style={{
|
||||
marginTop: 20,
|
||||
color: colors.r4,
|
||||
borderRadius: 4,
|
||||
fontSize: 15,
|
||||
}}
|
||||
>
|
||||
{getErrorMessage(error)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<form
|
||||
style={{ display: 'flex', flexDirection: 'row', marginTop: 30 }}
|
||||
onSubmit={onSubmit}
|
||||
<form
|
||||
style={{ display: 'flex', flexDirection: 'row', marginTop: 30 }}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Input
|
||||
autoFocus={true}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
style={{ flex: 1, marginRight: 10 }}
|
||||
/>
|
||||
<ButtonWithLoading primary loading={loading} style={{ fontSize: 15 }}>
|
||||
Sign in
|
||||
</ButtonWithLoading>
|
||||
</form>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
marginTop: 15,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
bare
|
||||
style={{ fontSize: 15, color: colors.b4, marginLeft: 10 }}
|
||||
onClick={onDemo}
|
||||
>
|
||||
<Input
|
||||
autoFocus={true}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
style={{ flex: 1, marginRight: 10 }}
|
||||
/>
|
||||
<ButtonWithLoading primary loading={loading} style={{ fontSize: 15 }}>
|
||||
Sign in
|
||||
</ButtonWithLoading>
|
||||
</form>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
marginTop: 15,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
bare
|
||||
style={{ fontSize: 15, color: colors.b4, marginLeft: 10 }}
|
||||
onClick={onDemo}
|
||||
>
|
||||
Try Demo →
|
||||
</Button>
|
||||
</View>
|
||||
Try Demo →
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -162,15 +162,13 @@ function SchedulePreview({ previewDates }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
direction="column"
|
||||
spacing={1}
|
||||
style={{ marginTop: 15, color: colors.n4 }}
|
||||
>
|
||||
{content}
|
||||
</Stack>
|
||||
</>
|
||||
<Stack
|
||||
direction="column"
|
||||
spacing={1}
|
||||
style={{ marginTop: 15, color: colors.n4 }}
|
||||
>
|
||||
{content}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
import { useViewportSize } from '@react-aria/utils';
|
||||
import { css } from 'glamor';
|
||||
|
||||
import * as Platform from 'loot-core/src/client/platform';
|
||||
@@ -10,6 +9,7 @@ import Add from '../icons/v1/Add';
|
||||
import CheveronDown from '../icons/v1/CheveronDown';
|
||||
import CheveronRight from '../icons/v1/CheveronRight';
|
||||
import Cog from '../icons/v1/Cog';
|
||||
import Pin from '../icons/v1/Pin';
|
||||
import Reports from '../icons/v1/Reports';
|
||||
import StoreFrontIcon from '../icons/v1/StoreFront';
|
||||
import TuningIcon from '../icons/v1/Tuning';
|
||||
@@ -17,7 +17,6 @@ import Wallet from '../icons/v1/Wallet';
|
||||
import ArrowButtonLeft1 from '../icons/v2/ArrowButtonLeft1';
|
||||
import CalendarIcon from '../icons/v2/Calendar';
|
||||
import { styles, colors } from '../style';
|
||||
import { breakpoints } from '../tokens';
|
||||
|
||||
import {
|
||||
View,
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
ButtonLink,
|
||||
Button,
|
||||
} from './common';
|
||||
import { useSidebar } from './FloatableSidebar';
|
||||
import { useDraggable, useDroppable, DropHighlight } from './sort.js';
|
||||
import CellValue from './spreadsheet/CellValue';
|
||||
|
||||
@@ -460,11 +460,25 @@ function Accounts({
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleButton({ style, onFloat }) {
|
||||
function ToggleButton({ style, isFloating, onFloat }) {
|
||||
return (
|
||||
<View className="float" style={[style, { flexShrink: 0 }]}>
|
||||
<Button bare onClick={onFloat}>
|
||||
<ArrowButtonLeft1 style={{ width: 13, height: 13, color: colors.n5 }} />
|
||||
{isFloating ? (
|
||||
<Pin
|
||||
style={{
|
||||
margin: -2,
|
||||
width: 15,
|
||||
height: 15,
|
||||
color: colors.n5,
|
||||
transform: 'rotate(45deg)',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ArrowButtonLeft1
|
||||
style={{ width: 13, height: 13, color: colors.n5 }}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
@@ -539,8 +553,7 @@ export function Sidebar({
|
||||
}) {
|
||||
let hasWindowButtons = !Platform.isBrowser && Platform.OS === 'mac';
|
||||
|
||||
let windowWidth = useViewportSize().width;
|
||||
let sidebarAlwaysFloats = windowWidth < breakpoints.medium;
|
||||
const sidebar = useSidebar();
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -550,9 +563,9 @@ export function Sidebar({
|
||||
color: colors.n9,
|
||||
backgroundColor: colors.n1,
|
||||
'& .float': {
|
||||
opacity: 0,
|
||||
opacity: isFloating ? 1 : 0,
|
||||
transition: 'opacity .25s, width .25s',
|
||||
width: hasWindowButtons ? null : 0,
|
||||
width: hasWindowButtons || isFloating ? null : 0,
|
||||
},
|
||||
'&:hover .float': {
|
||||
opacity: 1,
|
||||
@@ -562,7 +575,7 @@ export function Sidebar({
|
||||
style,
|
||||
]}
|
||||
>
|
||||
{hasWindowButtons && !sidebarAlwaysFloats && (
|
||||
{hasWindowButtons && !sidebar.alwaysFloats && (
|
||||
<ToggleButton
|
||||
style={[
|
||||
{
|
||||
@@ -574,6 +587,7 @@ export function Sidebar({
|
||||
paddingRight: 8,
|
||||
},
|
||||
]}
|
||||
isFloating={isFloating}
|
||||
onFloat={onFloat}
|
||||
/>
|
||||
)}
|
||||
@@ -614,8 +628,8 @@ export function Sidebar({
|
||||
|
||||
<View style={{ flex: 1, flexDirection: 'row' }} />
|
||||
|
||||
{!hasWindowButtons && !sidebarAlwaysFloats && (
|
||||
<ToggleButton onFloat={onFloat} />
|
||||
{!hasWindowButtons && !sidebar.alwaysFloats && (
|
||||
<ToggleButton isFloating={isFloating} onFloat={onFloat} />
|
||||
)}
|
||||
</View>
|
||||
|
||||
|
||||
6
upcoming-release-notes/868.md
Normal file
6
upcoming-release-notes/868.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [j-f1]
|
||||
---
|
||||
|
||||
Improve sidebar auto-floating behavior
|
||||
Reference in New Issue
Block a user