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