diff --git a/packages/app/package.json b/packages/app/package.json
index f046556027..0470ef952a 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -17,6 +17,7 @@
"@emotion/react": "^11.10.4",
"@emotion/server": "^11.10.0",
"@emotion/styled": "^11.10.4",
+ "css-spring": "^4.1.0",
"lit": "^2.3.1",
"next": "12.3.1",
"prettier": "^2.7.1",
diff --git a/packages/app/public/globals.css b/packages/app/public/globals.css
index 5af0a061bd..62b1feadfd 100644
--- a/packages/app/public/globals.css
+++ b/packages/app/public/globals.css
@@ -2,7 +2,7 @@
-webkit-overflow-scrolling: touch;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
box-sizing: border-box;
- transition: all 0.1s;
+ /*transition: all 0.1s;*/
}
html,
body,
diff --git a/packages/app/src/components/Header/index.tsx b/packages/app/src/components/Header/index.tsx
index c4a02503a7..a082915dd5 100644
--- a/packages/app/src/components/Header/index.tsx
+++ b/packages/app/src/components/Header/index.tsx
@@ -20,33 +20,47 @@ import {
import { Popover } from '@/components/popover';
import { useTheme } from '@/styles';
import { useEditor } from '@/components/editor-provider';
+import { AnimateRadio } from '@/components/animate-radio';
-const EditorModeSwitch = () => {
- const [mode, setMode] = useState<'page' | 'edgeless'>('page');
+const PaperItem = ({ active }: { active?: boolean }) => {
+ const {
+ theme: {
+ colors: { highlight, disabled },
+ },
+ } = useTheme();
+ return ;
+};
+
+const EdgelessItem = ({ active }: { active?: boolean }) => {
+ const {
+ theme: {
+ colors: { highlight, disabled },
+ },
+ } = useTheme();
+
+ return ;
+};
+const EditorModeSwitch = ({ isHover }: { isHover: boolean }) => {
const handleModeSwitch = (mode: 'page' | 'edgeless') => {
const event = new CustomEvent('affine.switch-mode', { detail: mode });
window.dispatchEvent(event);
-
- setMode(mode);
};
return (
-
- {
- handleModeSwitch('page');
- }}
- style={{ cursor: 'pointer' }}
- >
- {
- handleModeSwitch('edgeless');
- }}
- style={{ cursor: 'pointer' }}
- >
-
+ }
+ labelRight="Edgeless"
+ iconRight={}
+ style={{
+ marginRight: '12px',
+ }}
+ initialValue="left"
+ onChange={value => {
+ handleModeSwitch(value === 'left' ? 'page' : 'edgeless');
+ }}
+ />
);
};
@@ -102,6 +116,8 @@ const PopoverContent = () => {
export const Header = () => {
const [title, setTitle] = useState('');
+ const [isHover, setIsHover] = useState(false);
+
const { editor } = useEditor();
useEffect(() => {
@@ -114,12 +130,19 @@ export const Header = () => {
}, [editor]);
return (
-
+ {
+ setIsHover(true);
+ }}
+ onMouseLeave={() => {
+ setIsHover(false);
+ }}
+ >
{}} />
-
+
{title}
diff --git a/packages/app/src/components/animate-radio/icons.tsx b/packages/app/src/components/animate-radio/icons.tsx
new file mode 100644
index 0000000000..cc93ced81f
--- /dev/null
+++ b/packages/app/src/components/animate-radio/icons.tsx
@@ -0,0 +1,38 @@
+import { CSSProperties, DOMAttributes } from 'react';
+
+type IconProps = {
+ color?: string;
+ style?: CSSProperties;
+} & DOMAttributes;
+
+export const ArrowIcon = ({
+ color,
+ style: propsStyle = {},
+ direction = 'right',
+ ...props
+}: IconProps & { direction?: 'left' | 'right' | 'middle' }) => {
+ const style = {
+ fill: color,
+ transform: `rotate(${direction === 'left' ? '0' : '180deg'})`,
+ opacity: direction === 'middle' ? 0 : 1,
+ ...propsStyle,
+ };
+ return (
+
+ );
+};
diff --git a/packages/app/src/components/animate-radio/index.tsx b/packages/app/src/components/animate-radio/index.tsx
new file mode 100644
index 0000000000..4919581f80
--- /dev/null
+++ b/packages/app/src/components/animate-radio/index.tsx
@@ -0,0 +1,151 @@
+import { useState, useEffect, cloneElement } from 'react';
+import {
+ StyledAnimateRadioContainer,
+ StyledRadioMiddle,
+ StyledMiddleLine,
+ StyledRadioItem,
+ StyledLabel,
+ StyledIcon,
+} from './style';
+import { ArrowIcon } from './icons';
+import type {
+ RadioItemStatus,
+ AnimateRadioProps,
+ AnimateRadioItemProps,
+} from './type';
+
+const AnimateRadioItem = ({
+ active,
+ status,
+ icon,
+ label,
+ isLeft,
+ ...props
+}: AnimateRadioItemProps) => {
+ return (
+
+
+ {cloneElement(icon, {
+ active,
+ })}
+
+
+ {label}
+
+ );
+};
+
+const RadioMiddle = ({
+ isHover,
+ direction,
+}: {
+ isHover: boolean;
+ direction: 'left' | 'right' | 'middle';
+}) => {
+ return (
+
+
+
+
+ );
+};
+
+export const AnimateRadio = ({
+ labelLeft,
+ labelRight,
+ iconLeft,
+ iconRight,
+ isHover,
+ style = {},
+ onChange,
+ initialValue = 'left',
+}: AnimateRadioProps) => {
+ const [active, setActive] = useState(initialValue);
+ const modifyRadioItemStatus = (): RadioItemStatus => {
+ return {
+ left: !isHover && active === 'right' ? 'shrink' : 'normal',
+ right: !isHover && active === 'left' ? 'shrink' : 'normal',
+ };
+ };
+ const [radioItemStatus, setRadioItemStatus] = useState(
+ modifyRadioItemStatus
+ );
+
+ useEffect(() => {
+ setRadioItemStatus(modifyRadioItemStatus());
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isHover, active]);
+
+ return (
+
+ {
+ setActive('left');
+ onChange?.('left');
+ }}
+ onMouseEnter={() => {
+ setRadioItemStatus({
+ right: 'normal',
+ left: 'stretch',
+ });
+ }}
+ onMouseLeave={() => {
+ setRadioItemStatus({
+ ...radioItemStatus,
+ left: 'normal',
+ });
+ }}
+ />
+
+ {
+ setActive('right');
+ onChange?.('right');
+ }}
+ onMouseEnter={() => {
+ setRadioItemStatus({
+ left: 'normal',
+ right: 'stretch',
+ });
+ }}
+ onMouseLeave={() => {
+ setRadioItemStatus({
+ ...radioItemStatus,
+ right: 'normal',
+ });
+ }}
+ />
+
+ );
+};
+
+export default AnimateRadio;
diff --git a/packages/app/src/components/animate-radio/style.ts b/packages/app/src/components/animate-radio/style.ts
new file mode 100644
index 0000000000..8e5a6ec13e
--- /dev/null
+++ b/packages/app/src/components/animate-radio/style.ts
@@ -0,0 +1,152 @@
+import { keyframes, styled } from '@/styles';
+import spring, { toString } from 'css-spring';
+import type { ItemStatus } from './type';
+
+const ANIMATE_DURATION = 300;
+
+export const StyledAnimateRadioContainer = styled('div')<{ shrink: boolean }>(
+ ({ shrink }) => {
+ const animateScaleStretch = keyframes`${toString(
+ spring({ width: '66px' }, { width: '132px' }, { preset: 'gentle' })
+ )}`;
+ const animateScaleShrink = keyframes(
+ `${toString(
+ spring({ width: '132px' }, { width: '66px' }, { preset: 'gentle' })
+ )}`
+ );
+ const shrinkStyle = shrink
+ ? {
+ animation: `${animateScaleShrink} ${ANIMATE_DURATION}ms forwards`,
+ background: 'transparent',
+ }
+ : {
+ animation: `${animateScaleStretch} ${ANIMATE_DURATION}ms forwards`,
+ };
+
+ return {
+ height: '36px',
+ borderRadius: '18px',
+ background: '#F1F3FF',
+ position: 'relative',
+ display: 'flex',
+ transition: `background ${ANIMATE_DURATION}ms`,
+ ...shrinkStyle,
+ };
+ }
+);
+
+export const StyledRadioMiddle = styled('div')<{
+ hidden: boolean;
+}>(({ hidden }) => {
+ return {
+ width: '6px',
+ height: '100%',
+ position: 'relative',
+ opacity: hidden ? '0' : '1',
+ };
+});
+
+export const StyledMiddleLine = styled('div')<{ hidden: boolean }>(
+ ({ hidden }) => {
+ return {
+ width: '1px',
+ height: '16px',
+ background: '#D0D7E3',
+ position: 'absolute',
+ left: '0',
+ right: '0',
+ top: '0',
+ bottom: '0',
+ margin: 'auto',
+ opacity: hidden ? '0' : '1',
+ };
+ }
+);
+
+export const StyledRadioItem = styled('div')<{
+ status: ItemStatus;
+ active: boolean;
+}>(({ status, active, theme }) => {
+ const animateScaleStretch = keyframes`${toString(
+ spring({ width: '66px' }, { width: '116px' })
+ )}`;
+ const animateScaleOrigin = keyframes(
+ `${toString(spring({ width: '116px' }, { width: '66px' }))}`
+ );
+ const animateScaleShrink = keyframes(
+ `${toString(spring({ width: '66px' }, { width: '0px' }))}`
+ );
+ const dynamicStyle =
+ status === 'stretch'
+ ? {
+ animation: `${animateScaleStretch} ${ANIMATE_DURATION}ms forwards`,
+ flexShrink: '0',
+ }
+ : status === 'shrink'
+ ? {
+ animation: `${animateScaleShrink} ${ANIMATE_DURATION}ms forwards`,
+ opacity: '0',
+ }
+ : { animation: `${animateScaleOrigin} ${ANIMATE_DURATION}ms forwards` };
+
+ const {
+ colors: { highlight, disabled },
+ } = theme;
+ return {
+ height: '100%',
+ display: 'flex',
+ cursor: 'pointer',
+ overflow: 'hidden',
+ color: active ? highlight : disabled,
+ ...dynamicStyle,
+ };
+});
+
+export const StyledLabel = styled('div')<{
+ shrink: boolean;
+}>(({ shrink }) => {
+ const animateScaleStretch = keyframes`${toString(
+ spring({ scale: 0 }, { scale: 1 }, { preset: 'gentle' })
+ )}`;
+ const animateScaleShrink = keyframes(
+ `${toString(spring({ scale: 1 }, { scale: 0 }, { preset: 'gentle' }))}`
+ );
+ const shrinkStyle = shrink
+ ? {
+ animation: `${animateScaleShrink} ${ANIMATE_DURATION}ms forwards`,
+ }
+ : {
+ animation: `${animateScaleStretch} ${ANIMATE_DURATION}ms forwards`,
+ };
+
+ return {
+ display: 'flex',
+ alignItems: 'center',
+ fontSize: '16px',
+ flexShrink: '0',
+ transition: `transform ${ANIMATE_DURATION}ms`,
+ fontWeight: 'normal',
+ ...shrinkStyle,
+ };
+});
+
+export const StyledIcon = styled('div')<{
+ shrink: boolean;
+ isLeft: boolean;
+}>(({ shrink, isLeft }) => {
+ const shrinkStyle = shrink
+ ? {
+ width: '24px',
+ margin: isLeft ? '0 12px' : '0 5px',
+ }
+ : {
+ width: '66px',
+ };
+ return {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ flexShrink: '0',
+ ...shrinkStyle,
+ };
+});
diff --git a/packages/app/src/components/animate-radio/type.ts b/packages/app/src/components/animate-radio/type.ts
new file mode 100644
index 0000000000..1a2f1277d0
--- /dev/null
+++ b/packages/app/src/components/animate-radio/type.ts
@@ -0,0 +1,25 @@
+import { CSSProperties, DOMAttributes, ReactElement } from 'react';
+
+export type ItemStatus = 'normal' | 'stretch' | 'shrink';
+
+export type RadioItemStatus = {
+ left: ItemStatus;
+ right: ItemStatus;
+};
+export type AnimateRadioProps = {
+ labelLeft: string;
+ labelRight: string;
+ iconLeft: ReactElement;
+ iconRight: ReactElement;
+ isHover: boolean;
+ initialValue?: 'left' | 'right';
+ style?: CSSProperties;
+ onChange?: (value: 'left' | 'right') => void;
+};
+export type AnimateRadioItemProps = {
+ active: boolean;
+ status: ItemStatus;
+ label: string;
+ icon: ReactElement;
+ isLeft: boolean;
+} & DOMAttributes;
diff --git a/packages/app/src/styles/index.ts b/packages/app/src/styles/index.ts
index f1c02991ff..e74c167d3c 100644
--- a/packages/app/src/styles/index.ts
+++ b/packages/app/src/styles/index.ts
@@ -1,6 +1,6 @@
export type { ThemeMode, ThemeProviderProps, AffineTheme } from './types';
-export { styled } from './styled';
+export * from './styled';
export { ThemeProvider } from './themeProvider';
export { lightTheme, darkTheme } from './theme';
export { useTheme } from './hooks';
diff --git a/packages/app/src/styles/styled.ts b/packages/app/src/styles/styled.ts
index 11464cee29..7410fa1356 100644
--- a/packages/app/src/styles/styled.ts
+++ b/packages/app/src/styles/styled.ts
@@ -1,3 +1,3 @@
import emotionStyled from '@emotion/styled';
-
+export { css, keyframes } from '@emotion/react';
export const styled = emotionStyled;
diff --git a/packages/app/src/styles/theme.ts b/packages/app/src/styles/theme.ts
index de9c321b78..f419b8d646 100644
--- a/packages/app/src/styles/theme.ts
+++ b/packages/app/src/styles/theme.ts
@@ -3,25 +3,30 @@ import { AffineTheme, ThemeMode } from './types';
export const lightTheme: AffineTheme = {
colors: {
- primary: '#0070f3',
+ primary: '#3A4C5C',
+ highlight: '#7389FD',
+ disabled: '#9096A5',
+ background: '#fff',
},
};
export const darkTheme: AffineTheme = {
colors: {
- primary: '#000',
+ primary: '#fff',
+ highlight: '#7389FD',
+ disabled: '#9096A5',
+ background: '#3d3c3f',
},
};
export const globalThemeConstant = (mode: ThemeMode, theme: AffineTheme) => {
const isDark = mode === 'dark';
return {
- '--color-primary': theme.colors.primary,
- '--page-background-color': isDark ? '#3d3c3f' : '#fff',
- '--page-text-color': isDark ? '#fff' : '#3a4c5c',
+ '--page-background-color': theme.colors.background,
+ '--page-text-color': theme.colors.primary,
// editor style variables
- '--affine-primary-color': isDark ? '#fff' : '#3a4c5c',
+ '--affine-primary-color': theme.colors.primary,
'--affine-muted-color': '#a6abb7',
'--affine-highlight-color': '#6880ff',
'--affine-placeholder-color': '#c7c7c7',
@@ -34,22 +39,3 @@ export const globalThemeConstant = (mode: ThemeMode, theme: AffineTheme) => {
'Roboto Mono, apple-system, BlinkMacSystemFont,Helvetica Neue, Tahoma, PingFang SC, Microsoft Yahei, Arial,Hiragino Sans GB, sans-serif, Apple Color Emoji, Segoe UI Emoji,Segoe UI Symbol, Noto Color Emoji',
};
};
-
-const editorStyleVariable = {
- '--affine-primary-color': '#3a4c5c',
- '--affine-muted-color': '#a6abb7',
- '--affine-highlight-color': '#6880ff',
- '--affine-placeholder-color': '#c7c7c7',
- '--affine-selected-color': 'rgba(104, 128, 255, 0.1)',
-
- '--affine-font-family':
- 'Avenir Next, apple-system, BlinkMacSystemFont,Helvetica Neue, Tahoma, PingFang SC, Microsoft Yahei, Arial,Hiragino Sans GB, sans-serif, Apple Color Emoji, Segoe UI Emoji,Segoe UI Symbol, Noto Color Emoji',
-
- '--affine-font-family2':
- 'Roboto Mono, apple-system, BlinkMacSystemFont,Helvetica Neue, Tahoma, PingFang SC, Microsoft Yahei, Arial,Hiragino Sans GB, sans-serif, Apple Color Emoji, Segoe UI Emoji,Segoe UI Symbol, Noto Color Emoji',
-};
-
-const pageStyleVariable = {
- '--page-background-color': '#fff',
- '--page-text-color': '#3a4c5c',
-};
diff --git a/packages/app/src/styles/types.ts b/packages/app/src/styles/types.ts
index 95c5bba951..81b9fa4cb4 100644
--- a/packages/app/src/styles/types.ts
+++ b/packages/app/src/styles/types.ts
@@ -14,6 +14,9 @@ export type ThemeProviderValue = {
export interface AffineTheme {
colors: {
primary: string;
+ highlight: string;
+ disabled: string;
+ background: string;
};
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3a92e582fc..9fada691b7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -28,6 +28,7 @@ importers:
'@types/node': 18.7.18
'@types/react': 18.0.20
'@types/react-dom': 18.0.6
+ css-spring: ^4.1.0
eslint: 8.22.0
eslint-config-next: 12.3.1
eslint-config-prettier: ^8.5.0
@@ -48,6 +49,7 @@ importers:
'@emotion/react': 11.10.4_w5j4k42lgipnm43s3brx6h3c34
'@emotion/server': 11.10.0_@emotion+css@11.10.0
'@emotion/styled': 11.10.4_yiaqs725o7pcd7rteavrnhgj4y
+ css-spring: 4.1.0
lit: 2.4.0
next: 12.3.1_biqbaboplfbrettd7655fr4n2y
prettier: 2.7.1
@@ -906,6 +908,12 @@ packages:
which: 2.0.2
dev: true
+ /css-spring/4.1.0:
+ resolution: {integrity: sha512-RdA4NuRNk2xChTSo+52P2jlfr+rUgNY94mV7uHrCeDPvaUtFZgW6LMoCy4xEX3HphZ7LLkCHiUY5PBSegFBE3A==}
+ dependencies:
+ lodash: 4.17.21
+ dev: false
+
/csstype/3.1.1:
resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
@@ -1172,7 +1180,7 @@ packages:
eslint-import-resolver-webpack:
optional: true
dependencies:
- '@typescript-eslint/parser': 5.38.0_eslint@8.22.0
+ '@typescript-eslint/parser': 5.38.0_76twfck5d7crjqrmw4yltga7zm
debug: 3.2.7
eslint: 8.22.0
eslint-import-resolver-node: 0.3.6
@@ -1191,7 +1199,7 @@ packages:
'@typescript-eslint/parser':
optional: true
dependencies:
- '@typescript-eslint/parser': 5.38.0_eslint@8.22.0
+ '@typescript-eslint/parser': 5.38.0_76twfck5d7crjqrmw4yltga7zm
array-includes: 3.1.5
array.prototype.flat: 1.3.0
debug: 2.6.9
@@ -1891,6 +1899,10 @@ packages:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true
+ /lodash/4.17.21:
+ resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+ dev: false
+
/loose-envify/1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true