chore: move client folders (#948)

This commit is contained in:
DarkSky
2023-02-10 20:41:01 +08:00
committed by GitHub
parent cb118149f3
commit 8a7393a961
235 changed files with 114 additions and 215 deletions

View File

@@ -0,0 +1,26 @@
import { NotFoundTitle, PageContainer } from './styles';
import { useTranslation } from '@affine/i18n';
import { Button } from '@affine/component';
import { useRouter } from 'next/router';
export const NotfoundPage = () => {
const { t } = useTranslation();
const router = useRouter();
return (
<PageContainer>
<NotFoundTitle data-testid="notFound">
{t('404 - Page Not Found')}
<p>
<Button
onClick={() => {
router.push('/workspace');
}}
>
{t('Back Home')}
</Button>
</p>
</NotFoundTitle>
</PageContainer>
);
};
export default NotfoundPage;

View File

@@ -0,0 +1,21 @@
import { styled } from '@affine/component';
export const PageContainer = styled('div')(({ theme }) => {
return {
width: '100%',
height: 'calc(100vh)',
backgroundColor: theme.colors.pageBackground,
};
});
export const NotFoundTitle = styled('h1')(({ theme }) => {
return {
position: 'relative',
top: 'calc(50% - 100px)',
height: '100px',
fontSize: '60px',
lineHeight: '100px',
color: theme.colors.textColor,
textAlign: 'center',
};
});

View File

@@ -0,0 +1,139 @@
export const LogoIcon = () => {
return (
<svg
width="50"
height="50"
viewBox="0 0 50 50"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M21.1996 0L4 50H14.0741L25.0146 15.4186L35.96 50H46L28.7978 0H21.1996Z"
/>
</svg>
);
};
export const DocIcon = () => {
return (
<svg
width="50"
height="50"
viewBox="0 0 50 50"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 40.5353V9.46462C2 6.95444 2.99716 4.54708 4.77212 2.77212C6.54708 0.997163 8.95444 0 11.4646 0H37.7552C39.0224 0 40.0497 1.02726 40.0497 2.29445V33.3652C40.0497 33.4357 40.0465 33.5055 40.0403 33.5744C39.9882 34.1502 39.7234 34.6646 39.3251 35.0385C38.9147 35.4237 38.3625 35.6597 37.7552 35.6597H11.4646C11.0129 35.6597 10.5676 35.7224 10.1404 35.8429C8.60419 36.2781 7.37011 37.4505 6.85245 38.9541C6.67955 39.4584 6.58891 39.9922 6.58891 40.5354C6.58891 41.8285 7.1026 43.0687 8.01697 43.983C8.93134 44.8974 10.1715 45.4111 11.4646 45.4111H42.6309V4.68456C42.6309 3.41736 43.6582 2.3901 44.9254 2.3901C46.1926 2.3901 47.2198 3.41736 47.2198 4.68456V47.7055C47.2198 48.9727 46.1926 50 44.9254 50H11.4646C8.95445 50 6.54708 49.0028 4.77212 47.2279C2.99716 45.4529 2 43.0456 2 40.5353ZM12.6596 38.2409C11.3925 38.2409 10.3652 39.2682 10.3652 40.5354C10.3652 41.8026 11.3925 42.8298 12.6596 42.8298H36.5602C37.8274 42.8298 38.8546 41.8026 38.8546 40.5354C38.8546 39.2682 37.8274 38.2409 36.5602 38.2409H12.6596Z"
/>
</svg>
);
};
export const TwitterIcon = () => {
return (
<svg
width="22"
height="22"
viewBox="0 0 22 22"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M21.6725 4.5259C20.9071 4.86507 20.0848 5.09423 19.2204 5.19782C20.1123 4.66413 20.7796 3.82416 21.0977 2.83465C20.2598 3.33239 19.3426 3.68274 18.3862 3.87048C17.7431 3.18378 16.8912 2.72861 15.9629 2.57567C15.0345 2.42272 14.0816 2.58054 13.2522 3.02463C12.4227 3.46872 11.7631 4.17423 11.3757 5.03163C10.9883 5.88902 10.8948 6.85034 11.1097 7.76632C9.41177 7.68106 7.75072 7.23974 6.23436 6.47098C4.71801 5.70222 3.38025 4.62321 2.30789 3.30398C1.94123 3.93648 1.73039 4.66982 1.73039 5.45082C1.72998 6.1539 1.90312 6.8462 2.23445 7.46632C2.56577 8.08644 3.04504 8.61518 3.62973 9.00565C2.95165 8.98407 2.28853 8.80085 1.69556 8.47123V8.52623C1.69549 9.51233 2.03659 10.4681 2.66098 11.2313C3.28536 11.9945 4.15458 12.5183 5.12114 12.7136C4.49211 12.8838 3.83262 12.9089 3.19248 12.7869C3.46518 13.6354 3.99639 14.3773 4.71173 14.9089C5.42707 15.4405 6.29073 15.7351 7.18181 15.7514C5.66916 16.9389 3.80104 17.583 1.87798 17.5801C1.53733 17.5802 1.19696 17.5603 0.858643 17.5206C2.81066 18.7756 5.08295 19.4417 7.40364 19.4391C15.2595 19.4391 19.5541 12.9326 19.5541 7.28965C19.5541 7.10632 19.5495 6.92115 19.5412 6.73782C20.3766 6.13371 21.0976 5.38564 21.6706 4.52865L21.6725 4.5259V4.5259Z" />
</svg>
);
};
export const GithubIcon = () => {
return (
<svg
width="22"
height="22"
viewBox="0 0 22 22"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_3073_4801)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.9999 0.000812531C4.92238 0.000812531 -0.00012207 4.92331 -0.00012207 11.0008C-0.00012207 15.8683 3.14863 19.9796 7.52113 21.4371C8.07113 21.5333 8.27738 21.2033 8.27738 20.9146C8.27738 20.6533 8.26363 19.7871 8.26363 18.8658C5.49988 19.3746 4.78488 18.1921 4.56488 17.5733C4.44113 17.2571 3.90488 16.2808 3.43738 16.0196C3.05238 15.8133 2.50238 15.3046 3.42363 15.2908C4.28988 15.2771 4.90863 16.0883 5.11488 16.4183C6.10488 18.0821 7.68613 17.6146 8.31863 17.3258C8.41488 16.6108 8.70363 16.1296 9.01988 15.8546C6.57238 15.5796 4.01488 14.6308 4.01488 10.4233C4.01488 9.22706 4.44113 8.23706 5.14238 7.46706C5.03238 7.19206 4.64738 6.06456 5.25238 4.55206C5.25238 4.55206 6.17363 4.26331 8.27738 5.67956C9.15738 5.43206 10.0924 5.30831 11.0274 5.30831C11.9624 5.30831 12.8974 5.43206 13.7774 5.67956C15.8811 4.24956 16.8024 4.55206 16.8024 4.55206C17.4074 6.06456 17.0224 7.19206 16.9124 7.46706C17.6136 8.23706 18.0399 9.21331 18.0399 10.4233C18.0399 14.6446 15.4686 15.5796 13.0211 15.8546C13.4199 16.1983 13.7636 16.8583 13.7636 17.8896C13.7636 19.3608 13.7499 20.5433 13.7499 20.9146C13.7499 21.2033 13.9561 21.5471 14.5061 21.4371C16.6898 20.6998 18.5873 19.2964 19.9316 17.4242C21.2758 15.5521 21.9992 13.3056 21.9999 11.0008C21.9999 4.92331 17.0774 0.000812531 10.9999 0.000812531Z"
/>
</g>
<defs>
<clipPath id="clip0_3073_4801">
<rect width="22" height="22" fill="white" />
</clipPath>
</defs>
</svg>
);
};
export const DiscordIcon = () => {
return (
<svg
width="22"
height="22"
viewBox="0 0 22 22"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_3073_4817)">
<path d="M18.6239 4.11773C17.2214 3.48523 15.718 3.01773 14.146 2.7519C14.132 2.74921 14.1175 2.75094 14.1045 2.75685C14.0915 2.76276 14.0807 2.77256 14.0735 2.7849C13.881 3.12315 13.6665 3.56406 13.5162 3.9124C11.8489 3.66328 10.1538 3.66328 8.48645 3.9124C8.31901 3.52635 8.13019 3.14993 7.92087 2.7849C7.91377 2.77241 7.90304 2.76239 7.8901 2.75617C7.87716 2.74994 7.86263 2.74781 7.84845 2.75006C6.27729 3.0159 4.77395 3.4834 3.37054 4.11681C3.35846 4.12188 3.34823 4.13051 3.3412 4.14156C0.488536 8.33531 -0.293381 12.4255 0.0907023 16.4643C0.0917711 16.4742 0.0948415 16.4838 0.0997268 16.4924C0.104612 16.5011 0.11121 16.5087 0.119119 16.5147C1.78415 17.727 3.64126 18.6504 5.6127 19.2464C5.62645 19.2506 5.64114 19.2506 5.65488 19.2464C5.66862 19.2421 5.68075 19.2338 5.6897 19.2226C6.11393 18.6552 6.48986 18.0533 6.81354 17.4231C6.81803 17.4145 6.82062 17.4051 6.82113 17.3953C6.82164 17.3856 6.82006 17.3759 6.81649 17.3669C6.81293 17.3579 6.80747 17.3497 6.80047 17.343C6.79348 17.3362 6.78512 17.331 6.77595 17.3278C6.18379 17.1048 5.61003 16.8357 5.05995 16.523C5.05007 16.5173 5.04174 16.5093 5.03571 16.4997C5.02969 16.49 5.02615 16.479 5.02542 16.4677C5.0247 16.4563 5.0268 16.4449 5.03155 16.4346C5.03629 16.4243 5.04353 16.4153 5.05262 16.4084C5.16812 16.3231 5.28362 16.2342 5.39362 16.1453C5.40352 16.1373 5.41545 16.1322 5.42807 16.1306C5.4407 16.129 5.45352 16.1309 5.46512 16.1361C9.06487 17.7531 12.9635 17.7531 16.521 16.1361C16.5327 16.1306 16.5456 16.1284 16.5584 16.1299C16.5712 16.1313 16.5834 16.1364 16.5935 16.1444C16.7035 16.2342 16.818 16.3231 16.9345 16.4084C16.9436 16.4151 16.951 16.424 16.9559 16.4342C16.9608 16.4445 16.9631 16.4558 16.9625 16.4672C16.962 16.4785 16.9586 16.4896 16.9528 16.4993C16.9469 16.509 16.9387 16.5172 16.929 16.523C16.3808 16.8383 15.8106 17.1051 15.212 17.3269C15.2028 17.3302 15.1945 17.3355 15.1875 17.3423C15.1805 17.3492 15.175 17.3574 15.1715 17.3665C15.1679 17.3756 15.1663 17.3854 15.1668 17.3952C15.1674 17.4049 15.17 17.4145 15.1745 17.4231C15.5045 18.0529 15.8821 18.6524 16.2974 19.2216C16.306 19.2333 16.318 19.2421 16.3318 19.2467C16.3456 19.2512 16.3605 19.2515 16.3744 19.2473C18.3492 18.653 20.2093 17.7291 21.8762 16.5147C21.8843 16.509 21.8912 16.5017 21.8962 16.4931C21.9013 16.4846 21.9045 16.4751 21.9055 16.4652C22.3639 11.7957 21.1374 7.73856 18.6523 4.1434C18.6462 4.13171 18.6361 4.1226 18.6239 4.11773ZM7.35162 14.0049C6.26812 14.0049 5.37437 13.025 5.37437 11.8232C5.37437 10.6206 6.2507 9.64156 7.35162 9.64156C8.46079 9.64156 9.34629 10.6288 9.32887 11.8232C9.32887 13.0259 8.45254 14.0049 7.35162 14.0049V14.0049ZM14.662 14.0049C13.5776 14.0049 12.6848 13.025 12.6848 11.8232C12.6848 10.6206 13.5602 9.64156 14.662 9.64156C15.7712 9.64156 16.6567 10.6288 16.6393 11.8232C16.6393 13.0259 15.7721 14.0049 14.662 14.0049V14.0049Z" />
</g>
<defs>
<clipPath id="clip0_3073_4817">
<rect width="22" height="22" fill="white" />
</clipPath>
</defs>
</svg>
);
};
export const TelegramIcon = () => {
return (
<svg
width="22"
height="22"
viewBox="0 0 22 22"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M20.1081 4.59007L17.0651 19.43C16.8356 20.4774 16.2369 20.7381 15.3861 20.2447L10.7496 16.7115L8.51231 18.9366C8.26473 19.1926 8.05766 19.4068 7.5805 19.4068L7.91361 14.5237L16.507 6.49394C16.8806 6.14947 16.4259 5.95862 15.9263 6.30309L5.30275 13.2203L0.729238 11.7401C-0.265591 11.4189 -0.283597 10.7113 0.936307 10.2179L18.8252 3.09117C19.6535 2.76998 20.3782 3.28203 20.1081 4.59007V4.59007Z" />
</svg>
);
};
export const RedditIcon = () => {
return (
<svg
width="22"
height="22"
viewBox="0 0 22 22"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M21.9987 11.3855C21.9987 11.8602 21.878 12.2919 21.6365 12.6807C21.3951 13.0694 21.0698 13.3661 20.6606 13.5707C20.7588 13.9471 20.8079 14.34 20.8079 14.7492C20.8079 16.0177 20.3721 17.1921 19.5005 18.2724C18.6289 19.3527 17.4402 20.2059 15.9343 20.832C14.4285 21.458 12.7916 21.7711 11.0239 21.7711C9.25614 21.7711 7.62138 21.458 6.11961 20.832C4.61784 20.2059 3.43116 19.3527 2.55956 18.2724C1.68796 17.1921 1.25216 16.0177 1.25216 14.7492C1.25216 14.3645 1.29717 13.9799 1.38719 13.5952C0.969808 13.3906 0.634263 13.0899 0.380558 12.6929C0.126853 12.296 0 11.8602 0 11.3855C0 10.7145 0.237337 10.1395 0.712011 9.66076C1.18669 9.18199 1.76366 8.94261 2.44293 8.94261C3.13858 8.94261 3.73192 9.2004 4.22296 9.716C6.00708 8.47203 8.11447 7.80912 10.5451 7.72728L11.9692 1.33145C11.9937 1.22506 12.0551 1.13913 12.1533 1.07366C12.2515 1.00818 12.3579 0.987724 12.4725 1.01228L17.0023 2.00664C17.1496 1.70383 17.3706 1.46035 17.6652 1.27621C17.9599 1.09207 18.2831 1 18.635 1C19.1425 1 19.5762 1.178 19.9363 1.53401C20.2964 1.89001 20.4765 2.32172 20.4765 2.82913C20.4765 3.33654 20.2964 3.7703 19.9363 4.13039C19.5762 4.49049 19.1425 4.67054 18.635 4.67054C18.1276 4.67054 17.6959 4.49254 17.3399 4.13653C16.9839 3.78053 16.8059 3.34882 16.8059 2.84141L12.7057 1.93298L11.429 7.72728C13.8842 7.80093 16.008 8.45566 17.8003 9.69145C18.275 9.19222 18.8601 8.94261 19.5558 8.94261C20.235 8.94261 20.812 9.18199 21.2867 9.66076C21.7614 10.1395 21.9987 10.7145 21.9987 11.3855ZM5.13139 13.8285C5.13139 14.3359 5.30939 14.7696 5.6654 15.1297C6.0214 15.4898 6.45311 15.6699 6.96052 15.6699C7.46793 15.6699 7.90169 15.4898 8.26178 15.1297C8.62188 14.7696 8.80193 14.3359 8.80193 13.8285C8.80193 13.3211 8.62188 12.8894 8.26178 12.5334C7.90169 12.1773 7.46793 11.9993 6.96052 11.9993C6.4613 11.9993 6.03163 12.1794 5.67154 12.5395C5.31144 12.8996 5.13139 13.3293 5.13139 13.8285ZM15.075 18.1865C15.165 18.0965 15.21 17.9901 15.21 17.8673C15.21 17.7445 15.165 17.6381 15.075 17.5481C14.9932 17.4663 14.8909 17.4254 14.7681 17.4254C14.6453 17.4254 14.5389 17.4663 14.4489 17.5481C14.1134 17.8919 13.6182 18.1456 12.9635 18.3092C12.3088 18.4729 11.6541 18.5548 10.9993 18.5548C10.3446 18.5548 9.6899 18.4729 9.03518 18.3092C8.38045 18.1456 7.88532 17.8919 7.54977 17.5481C7.45975 17.4663 7.35336 17.4254 7.2306 17.4254C7.10783 17.4254 7.00553 17.4663 6.92369 17.5481C6.83367 17.63 6.78866 17.7343 6.78866 17.8612C6.78866 17.988 6.83367 18.0965 6.92369 18.1865C7.27561 18.5384 7.76051 18.8166 8.37841 19.0212C8.9963 19.2258 9.49757 19.3466 9.88222 19.3834C10.2669 19.4202 10.6392 19.4386 10.9993 19.4386C11.3594 19.4386 11.7318 19.4202 12.1165 19.3834C12.5011 19.3466 13.0024 19.2258 13.6203 19.0212C14.2382 18.8166 14.7231 18.5384 15.075 18.1865ZM15.0382 15.6699C15.5456 15.6699 15.9773 15.4898 16.3333 15.1297C16.6893 14.7696 16.8673 14.3359 16.8673 13.8285C16.8673 13.3293 16.6872 12.8996 16.3272 12.5395C15.9671 12.1794 15.5374 11.9993 15.0382 11.9993C14.5308 11.9993 14.097 12.1773 13.7369 12.5334C13.3768 12.8894 13.1968 13.3211 13.1968 13.8285C13.1968 14.3359 13.3768 14.7696 13.7369 15.1297C14.097 15.4898 14.5308 15.6699 15.0382 15.6699Z" />
</svg>
);
};
export const LinkIcon = () => {
return (
<svg
width="14"
height="15"
viewBox="0 0 14 15"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9 2.19999C8.6134 2.19999 8.3 1.88659 8.3 1.49999C8.3 1.11339 8.6134 0.799988 9 0.799988H13C13.0949 0.799988 13.1854 0.818878 13.268 0.853103C13.3499 0.887011 13.4267 0.937153 13.4935 1.00353C13.494 1.00402 13.4945 1.00452 13.495 1.00501C13.4955 1.00551 13.496 1.006 13.4965 1.0065C13.5628 1.07327 13.613 1.15009 13.6469 1.23204C13.6811 1.31457 13.7 1.40507 13.7 1.49999V5.49999C13.7 5.88659 13.3866 6.19999 13 6.19999C12.6134 6.19999 12.3 5.88659 12.3 5.49999V3.18994L6.16164 9.3283C5.88828 9.60166 5.44506 9.60166 5.17169 9.3283C4.89833 9.05493 4.89833 8.61171 5.17169 8.33835L11.3101 2.19999H9ZM2.33334 4.19999C2.16537 4.19999 2.00428 4.26671 1.8855 4.38549C1.76673 4.50426 1.7 4.66535 1.7 4.83332V12.1667C1.7 12.3346 1.76673 12.4957 1.8855 12.6145C2.00428 12.7333 2.16537 12.8 2.33334 12.8H9.66667C9.83464 12.8 9.99573 12.7333 10.1145 12.6145C10.2333 12.4957 10.3 12.3346 10.3 12.1667V8.16665C10.3 7.78006 10.6134 7.46665 11 7.46665C11.3866 7.46665 11.7 7.78006 11.7 8.16665V12.1667C11.7 12.7059 11.4858 13.2231 11.1045 13.6044C10.7231 13.9858 10.2059 14.2 9.66667 14.2H2.33334C1.79406 14.2 1.27688 13.9858 0.895553 13.6044C0.514229 13.2231 0.300003 12.7059 0.300003 12.1667V4.83332C0.300003 4.29405 0.514228 3.77686 0.895553 3.39554C1.27688 3.01421 1.79406 2.79999 2.33334 2.79999H6.33334C6.71994 2.79999 7.03334 3.11339 7.03334 3.49999C7.03334 3.88659 6.71994 4.19999 6.33334 4.19999H2.33334Z"
/>
</svg>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

View File

@@ -0,0 +1,145 @@
import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component';
import {
LogoIcon,
DocIcon,
TwitterIcon,
GithubIcon,
DiscordIcon,
TelegramIcon,
RedditIcon,
LinkIcon,
} from './Icons';
import logo from './affine-text-logo.png';
import {
StyledBigLink,
StyledSmallLink,
StyledSubTitle,
StyledLeftContainer,
StyledRightContainer,
StyledContent,
StyledLogo,
StyledModalHeader,
StyledModalHeaderLeft,
StyledModalFooter,
} from './style';
import bg from '@/components/contact-modal/bg.png';
import { useTranslation } from '@affine/i18n';
const linkList = [
{
icon: <GithubIcon />,
title: 'GitHub',
link: 'https://github.com/toeverything/AFFiNE',
},
{
icon: <RedditIcon />,
title: 'Reddit',
link: 'https://www.reddit.com/r/Affine/',
},
{
icon: <TwitterIcon />,
title: 'Twitter',
link: 'https://twitter.com/AffineOfficial',
},
{
icon: <TelegramIcon />,
title: 'Telegram',
link: 'https://t.me/affineworkos',
},
{
icon: <DiscordIcon />,
title: 'Discord',
link: 'https://discord.gg/Arn7TqJBvG',
},
];
type TransitionsModalProps = {
open: boolean;
onClose: () => void;
};
export const ContactModal = ({
open,
onClose,
}: TransitionsModalProps): JSX.Element => {
const { t } = useTranslation();
const rightLinkList = [
{
icon: <LogoIcon />,
title: t('Official Website'),
subTitle: 'AFFiNE.pro',
link: 'https://affine.pro',
},
{
icon: <DocIcon />,
title: t('AFFiNE Community'),
subTitle: 'community.affine.pro',
link: 'https://community.affine.pro',
},
];
const date = new Date();
const year = date.getFullYear();
return (
<Modal open={open} onClose={onClose} data-testid="contact-us-modal-content">
<ModalWrapper
width={860}
height={540}
style={{ backgroundImage: `url(${bg.src})` }}
>
<StyledModalHeader>
<StyledModalHeaderLeft>
<StyledLogo src={logo.src} alt="" />
<span>Alpha</span>
</StyledModalHeaderLeft>
<ModalCloseButton
onClick={() => {
onClose();
}}
/>
</StyledModalHeader>
<StyledContent>
<StyledLeftContainer>
{rightLinkList.map(({ icon, title, subTitle, link }) => {
return (
<StyledBigLink key={title} href={link} target="_blank">
{icon}
<p>{title}</p>
<p>
{subTitle}
<LinkIcon />
</p>
</StyledBigLink>
);
})}
</StyledLeftContainer>
<StyledRightContainer>
<StyledSubTitle>{t('Get in touch!')}</StyledSubTitle>
{linkList.map(({ icon, title, link }) => {
return (
<StyledSmallLink key={title} href={link} target="_blank">
{icon}
{title}
</StyledSmallLink>
);
})}
</StyledRightContainer>
</StyledContent>
<StyledModalFooter>
<p>
<a
href="https://affine.pro/content/blog/affine-alpha-is-coming/index"
target="_blank"
rel="noreferrer"
>
{t('How is AFFiNE Alpha different?')}
</a>
</p>
<p>Copyright &copy; {year} Toeverything</p>
</StyledModalFooter>
</ModalWrapper>
</Modal>
);
};
export default ContactModal;

View File

@@ -0,0 +1,168 @@
import { absoluteCenter, displayFlex, styled } from '@affine/component';
export const StyledBigLink = styled('a')(({ theme }) => {
return {
width: '334px',
height: '100px',
marginBottom: '48px',
paddingLeft: '90px',
fontSize: '24px',
lineHeight: '36px',
fontWeight: '600',
color: theme.colors.textColor,
borderRadius: '10px',
flexDirection: 'column',
...displayFlex('center'),
position: 'relative',
transition: 'background .15s',
':visited': {
color: theme.colors.textColor,
},
':hover': {
background: 'rgba(68, 97, 242, 0.1)',
},
':last-of-type': {
marginBottom: 0,
},
svg: {
width: '50px',
height: '50px',
marginRight: '40px',
color: theme.colors.primaryColor,
...absoluteCenter({ vertical: true, position: { left: '20px' } }),
},
p: {
width: '100%',
height: '30px',
lineHeight: '30px',
...displayFlex('flex-start', 'center'),
':not(:last-of-type)': {
marginBottom: '4px',
},
':first-of-type': {
fontSize: '22px',
},
':last-of-type': {
fontSize: '20px',
color: theme.colors.primaryColor,
},
svg: {
width: '15px',
height: '15px',
position: 'static',
transform: 'translate(0,0)',
marginLeft: '5px',
},
},
};
});
export const StyledSmallLink = styled('a')(({ theme }) => {
return {
width: '214px',
height: '37px',
display: 'flex',
alignItems: 'center',
fontSize: '18px',
fontWeight: '500',
paddingLeft: '24px',
borderRadius: '5px',
color: theme.colors.textColor,
transition: 'background .15s, color .15s',
':visited': {
color: theme.colors.textColor,
},
':hover': {
color: theme.colors.primaryColor,
background: theme.colors.hoverBackground,
},
svg: {
width: '22px',
marginRight: '30px',
color: theme.colors.primaryColor,
},
};
});
export const StyledSubTitle = styled('div')(({ theme }) => {
return {
width: '190px',
fontSize: '18px',
fontWeight: '600',
color: theme.colors.textColor,
marginBottom: '24px',
};
});
export const StyledLeftContainer = styled('div')({
width: '320px',
flexDirection: 'column',
...displayFlex('space-between', 'center'),
});
export const StyledRightContainer = styled('div')({
width: '214px',
flexShrink: '0',
flexDirection: 'column',
...displayFlex('center', 'flex-end'),
});
export const StyledContent = styled('div')({
height: '276px',
width: '100%',
padding: '0 140px',
...displayFlex('space-between', 'center'),
color: '#3A4C5C',
marginTop: '58px',
letterSpacing: '0.06em',
});
export const StyledLogo = styled('img')({
height: '18px',
width: 'auto',
});
export const StyledModalHeader = styled('div')(() => {
return {
height: '20px',
marginTop: '36px',
padding: '0 48px',
...displayFlex('space-between', 'center'),
};
});
export const StyledModalHeaderLeft = styled('div')(({ theme }) => {
return {
color: theme.colors.primaryColor,
...displayFlex('flex-end', 'flex-end'),
span: {
height: '20px',
border: `1px solid ${theme.colors.primaryColor}`,
borderRadius: '10px',
padding: '0 8px',
lineHeight: '26px',
fontSize: '14px',
marginLeft: '12px',
...displayFlex('center', 'center'),
},
};
});
export const StyledModalFooter = styled('div')(({ theme }) => {
return {
fontSize: '14px',
lineHeight: '20px',
textAlign: 'center',
color: theme.colors.textColor,
marginTop: '40px',
'p:first-of-type': {
color: theme.colors.primaryColor,
letterSpacing: '0.06em',
marginBottom: '25px',
a: {
':visited': {
color: theme.colors.linkColor,
},
},
},
};
});

View File

@@ -0,0 +1,123 @@
import { styled } from '@affine/component';
import { Modal, ModalWrapper, ModalCloseButton } from '@affine/component';
import { Button } from '@affine/component';
import { useState } from 'react';
import { Input } from '@affine/component';
import { KeyboardEvent } from 'react';
import { useTranslation } from '@affine/i18n';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { useRouter } from 'next/router';
import { toast } from '@affine/component';
interface ModalProps {
open: boolean;
onClose: () => void;
}
export const CreateWorkspaceModal = ({ open, onClose }: ModalProps) => {
const [workspaceName, setWorkspaceName] = useState('');
const [loading, setLoading] = useState(false);
const { createWorkspace } = useWorkspaceHelper();
const router = useRouter();
const handleCreateWorkspace = async () => {
setLoading(true);
const workspace = await createWorkspace(workspaceName);
if (workspace && workspace.id) {
setLoading(false);
router.replace(`/workspace/${workspace.id}`);
onClose();
} else {
toast('create error');
}
};
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
// 👇 Get input value
handleCreateWorkspace();
}
};
const { t } = useTranslation();
return (
<div>
<Modal open={open} onClose={onClose}>
<ModalWrapper width={560} height={342} style={{ padding: '10px' }}>
<Header>
<ModalCloseButton
top={6}
right={6}
onClick={() => {
onClose();
}}
/>
</Header>
<Content>
<ContentTitle>{t('New Workspace')}</ContentTitle>
<p>{t('Workspace description')}</p>
<Input
onKeyDown={handleKeyDown}
placeholder={t('Set a Workspace name')}
maxLength={15}
minLength={0}
onChange={value => {
setWorkspaceName(value);
}}
></Input>
<Button
disabled={!workspaceName}
style={{
width: '260px',
textAlign: 'center',
marginTop: '16px',
opacity: !workspaceName ? 0.5 : 1,
}}
loading={loading}
type="primary"
onClick={() => {
handleCreateWorkspace();
}}
>
{t('Create')}
</Button>
</Content>
</ModalWrapper>
</Modal>
</div>
);
};
const Header = styled('div')({
position: 'relative',
height: '44px',
});
const Content = styled('div')(({ theme }) => {
return {
padding: '0 84px',
textAlign: 'center',
fontSize: '18px',
lineHeight: '26px',
color: theme.colors.inputColor,
p: {
marginTop: '12px',
marginBottom: '16px',
},
};
});
const ContentTitle = styled('div')(() => {
return {
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'center',
paddingBottom: '16px',
};
});
// const Footer = styled('div')({
// height: '70px',
// paddingLeft: '24px',
// marginTop: '32px',
// textAlign: 'center',
// });

View File

@@ -0,0 +1,95 @@
import { styled } from '@affine/component';
import { Modal, ModalWrapper, ModalCloseButton } from '@affine/component';
import { Button } from '@affine/component';
import { Input } from '@affine/component';
import { useState } from 'react';
interface LoginModalProps {
open: boolean;
onClose: () => void;
workSpaceName: string;
}
export const DeleteModal = ({
open,
onClose,
workSpaceName,
}: LoginModalProps) => {
const [canDelete, setCanDelete] = useState<boolean>(true);
const InputChange = (value: string) => {
if (value === workSpaceName) {
setCanDelete(false);
} else {
setCanDelete(true);
}
};
return (
<div>
<Modal open={open} onClose={onClose}>
<ModalWrapper width={620} height={334}>
<Header>
<ModalCloseButton
onClick={() => {
onClose();
}}
/>
</Header>
<Content>
<ContentTitle>Delete Workspace</ContentTitle>
<div>
This action cannot be undone. This will permanently delete{' '}
{workSpaceName} workspace name along with all its content.
</div>
<Input
onChange={InputChange}
placeholder="Please type “delete” to confirm"
></Input>
</Content>
<Footer>
<Button
style={{ marginRight: '12px' }}
shape="circle"
onClick={() => {
onClose();
}}
>
Cancel
</Button>
<Button shape="circle" type="danger" disabled={canDelete}>
Delete
</Button>
</Footer>
</ModalWrapper>
</Modal>
</div>
);
};
const Header = styled('div')({
position: 'relative',
height: '44px',
});
const Content = styled('div')({
display: 'flex',
padding: '0 48px',
flexDirection: 'column',
alignItems: 'center',
gap: '16px',
});
const ContentTitle = styled('h1')({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'center',
paddingBottom: '16px',
});
const Footer = styled('div')({
height: '70px',
paddingLeft: '24px',
marginTop: '32px',
textAlign: 'center',
});

View File

@@ -0,0 +1,151 @@
export const SelectIcon = () => {
return (
<svg
width="36"
height="36"
viewBox="0 0 36 36"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_3076_4847)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11.6091 10.2222C11.61 8.86837 13.1871 8.12745 14.2297 8.99105L25.8121 18.5849C27.0294 19.5932 26.2062 21.4939 24.7271 21.4744C23.7186 21.4611 22.5692 21.4841 21.496 21.5947C20.4058 21.707 19.4662 21.9034 18.8355 22.1997C18.2047 22.4961 17.4537 23.0939 16.6713 23.8612C15.9009 24.6167 15.1495 25.4868 14.5159 26.2714C13.5867 27.4223 11.5982 26.8425 11.5992 25.2619L11.6091 10.2222ZM13.2091 10.2232L13.1992 25.263C13.1992 25.2637 13.1992 25.2644 13.1992 25.2651C13.2029 25.2676 13.2091 25.2712 13.2183 25.2743C13.237 25.2806 13.2518 25.2793 13.2576 25.2779C13.2597 25.2773 13.2604 25.2769 13.2607 25.2767L13.2607 25.2767C13.2608 25.2766 13.2645 25.2744 13.2711 25.2663C13.9256 24.4557 14.72 23.5339 15.551 22.7189C16.3698 21.9159 17.2757 21.1647 18.1551 20.7516C19.0346 20.3384 20.1911 20.1207 21.3319 20.0031C22.4897 19.8838 23.7064 19.8608 24.7481 19.8745C24.7586 19.8747 24.7627 19.8732 24.7628 19.8732L24.7628 19.8732C24.7632 19.8731 24.7639 19.8728 24.7657 19.8715C24.7705 19.868 24.7809 19.8575 24.788 19.839C24.7915 19.8299 24.7927 19.8229 24.7931 19.8185C24.7926 19.818 24.7921 19.8176 24.7915 19.8171L13.2091 10.2232Z"
/>
</g>
<defs>
<clipPath id="clip0_3076_4847">
<rect
width="24"
height="24"
fill="white"
transform="translate(6 6)"
/>
</clipPath>
</defs>
</svg>
);
};
export const TextIcon = () => {
return (
<svg
width="36"
height="36"
viewBox="0 0 36 36"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M27 9H9V11L17 11L17 27H19L19 11L27 11V9Z"
/>
</svg>
);
};
export const ShapeIcon = () => {
return (
<svg
width="36"
height="36"
viewBox="0 0 36 36"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M25.2284 12.9441L17.822 10.9596L15.8375 18.3659L17.451 18.7982L16.7206 20.259L13.8779 19.4973L16.6907 9L27.188 11.8127L24.3752 22.31L23.1393 21.9789L22.183 20.0662L23.2438 20.3504L25.2284 12.9441ZM14.981 13.0626C14.8225 13.0489 14.6621 13.0419 14.5 13.0419C11.4624 13.0419 9 15.5043 9 18.5419C9 21.5794 11.4624 24.0419 14.5 24.0419C14.6122 24.0419 14.7236 24.0385 14.8341 24.0319L15.7277 22.2447C15.3417 22.3726 14.9289 22.4419 14.5 22.4419C12.3461 22.4419 10.6 20.6958 10.6 18.5419C10.6 16.388 12.3461 14.6419 14.5 14.6419C14.5193 14.6419 14.5385 14.642 14.5578 14.6423L14.981 13.0626ZM19.5 16.0419L14 27.0419H25L19.5 16.0419ZM19.5 19.6196L16.5889 25.4419H22.4111L19.5 19.6196Z"
/>
</svg>
);
};
export const PenIcon = () => {
return (
<svg
width="36"
height="36"
viewBox="0 0 36 36"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12.0153 23.0152L12.3988 21.8646L23.1317 11.1317C23.6114 10.6519 24.3892 10.6519 24.869 11.1317C25.3487 11.6114 25.3487 12.3892 24.869 12.8689L14.136 23.6019L12.9854 23.9854L12.0153 23.0152ZM11.1339 20.8668L22.0003 10.0003C23.1049 8.89573 24.8958 8.89573 26.0003 10.0003C27.1049 11.1049 27.1049 12.8957 26.0003 14.0003L15.1339 24.8668C15.0461 24.9546 14.939 25.0207 14.8212 25.06L10.5182 26.4943C9.89283 26.7028 9.29784 26.1078 9.5063 25.4824L10.9406 21.1795C10.9799 21.0616 11.0461 20.9546 11.1339 20.8668Z"
/>
</svg>
);
};
export const StickerIcon = () => {
return (
<svg
width="36"
height="36"
viewBox="0 0 36 36"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M23.75 9C24.612 9 25.4386 9.34241 26.0481 9.9519C26.6576 10.5614 27 11.388 27 12.25V19.129C26.9997 19.7254 26.7627 20.2973 26.341 20.719L20.72 26.341C20.5111 26.5499 20.263 26.7157 19.99 26.8287C19.7171 26.9418 19.4245 27 19.129 27H12.25C11.388 27 10.5614 26.6576 9.9519 26.0481C9.34241 25.4386 9 24.612 9 23.75V12.25C9 11.388 9.34241 10.5614 9.9519 9.9519C10.5614 9.34241 11.388 9 12.25 9H23.75ZM23.75 10.5H12.25C11.7859 10.5 11.3408 10.6844 11.0126 11.0126C10.6844 11.3408 10.5 11.7859 10.5 12.25V23.75C10.5 24.716 11.284 25.5 12.25 25.5H19V22.25C18.9999 21.4199 19.3176 20.6212 19.8877 20.0178C20.4578 19.4144 21.2372 19.052 22.066 19.005L22.25 19H25.5V12.25C25.5 11.7859 25.3156 11.3408 24.9874 11.0126C24.6592 10.6844 24.2141 10.5 23.75 10.5ZM24.439 20.5H22.25C21.8107 20.5 21.3874 20.6653 21.0643 20.963C20.7412 21.2608 20.5419 21.6691 20.506 22.107L20.5 22.25V24.439L24.439 20.5Z" />
</svg>
);
};
export const ConnectorIcon = () => {
return (
<svg
width="36"
height="36"
viewBox="0 0 36 36"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M21.6 12.5C21.6 13.5493 22.4507 14.4 23.5 14.4C24.5493 14.4 25.4 13.5493 25.4 12.5C25.4 11.4507 24.5493 10.6 23.5 10.6C22.4507 10.6 21.6 11.4507 21.6 12.5ZM23.5 9C21.567 9 20 10.567 20 12.5C20 13.2108 20.2119 13.872 20.5759 14.4241L14.4241 20.5759C13.872 20.2119 13.2108 20 12.5 20C10.567 20 9 21.567 9 23.5C9 25.433 10.567 27 12.5 27C14.433 27 16 25.433 16 23.5C16 22.8575 15.8269 22.2555 15.5248 21.738L21.738 15.5248C22.2555 15.8269 22.8575 16 23.5 16C25.433 16 27 14.433 27 12.5C27 10.567 25.433 9 23.5 9ZM10.6 23.5C10.6 24.5493 11.4507 25.4 12.5 25.4C13.5493 25.4 14.4 24.5493 14.4 23.5C14.4 22.4507 13.5493 21.6 12.5 21.6C11.4507 21.6 10.6 22.4507 10.6 23.5Z"
/>
</svg>
);
};
export const UndoIcon = () => {
return (
<svg
width="36"
height="35"
viewBox="0 0 36 35"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.8277 12.2008C15.0955 12.4686 15.0955 12.9028 14.8277 13.1706L12.3412 15.6571H20.9551C21.6982 15.6571 22.2977 15.6571 22.7831 15.6968C23.2828 15.7376 23.7218 15.8239 24.128 16.0308C24.7731 16.3595 25.2976 16.884 25.6263 17.5292C25.8332 17.9353 25.9195 18.3743 25.9604 18.8741C26 19.3595 26 19.9589 26 20.7021V21.8286C26 22.2073 25.693 22.5143 25.3143 22.5143C24.9356 22.5143 24.6286 22.2073 24.6286 21.8286V20.7314C24.6286 19.952 24.628 19.4087 24.5935 18.9858C24.5596 18.5708 24.4964 18.3324 24.4044 18.1518C24.2071 17.7647 23.8924 17.45 23.5054 17.2528C23.3248 17.1608 23.0864 17.0976 22.6714 17.0637C22.2484 17.0291 21.7051 17.0286 20.9257 17.0286H12.3412L14.8277 19.5151C15.0955 19.7829 15.0955 20.2171 14.8277 20.4849C14.5599 20.7527 14.1258 20.7527 13.858 20.4849L10.2008 16.8277C9.93305 16.5599 9.93305 16.1258 10.2008 15.858L13.858 12.2008C14.1258 11.9331 14.5599 11.9331 14.8277 12.2008Z"
/>
</svg>
);
};
export const RedoIcon = () => {
return (
<svg
width="36"
height="36"
viewBox="0 0 36 36"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M21.1723 12.715C21.4401 12.4472 21.8742 12.4472 22.142 12.715L25.7992 16.3721C26.0669 16.6399 26.0669 17.0741 25.7992 17.3419L22.142 20.999C21.8742 21.2668 21.4401 21.2668 21.1723 20.999C20.9045 20.7312 20.9045 20.2971 21.1723 20.0293L23.6588 17.5427H15.0743C14.2949 17.5427 13.7516 17.5433 13.3286 17.5778C12.9136 17.6117 12.6752 17.6749 12.4946 17.7669C12.1076 17.9642 11.7929 18.2789 11.5956 18.666C11.5036 18.8465 11.4404 19.085 11.4065 19.4999C11.372 19.9229 11.3714 20.4662 11.3714 21.2456V22.3427C11.3714 22.7214 11.0644 23.0284 10.6857 23.0284C10.307 23.0284 10 22.7214 10 22.3427L10 21.2162C9.99999 20.4731 9.99999 19.8736 10.0396 19.3882C10.0805 18.8885 10.1668 18.4495 10.3737 18.0433C10.7024 17.3982 11.2269 16.8737 11.872 16.545C12.2782 16.3381 12.7172 16.2518 13.2169 16.2109C13.7023 16.1713 14.3018 16.1713 15.0449 16.1713H23.6588L21.1723 13.6847C20.9045 13.417 20.9045 12.9828 21.1723 12.715Z"
/>
</svg>
);
};

View File

@@ -0,0 +1,162 @@
import { useState, useEffect } from 'react';
import {
StyledEdgelessToolbar,
StyledToolbarWrapper,
StyledToolbarItem,
} from './style';
import {
SelectIcon,
TextIcon,
ShapeIcon,
PenIcon,
StickerIcon,
ConnectorIcon,
UndoIcon,
RedoIcon,
} from './Icons';
import { MuiSlide } from '@affine/component';
import { Tooltip } from '@affine/component';
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
import { useAppState } from '@/providers/app-state-provider';
import useHistoryUpdated from '@/hooks/use-history-update';
import { useTranslation } from '@affine/i18n';
const useToolbarList1 = () => {
const { t } = useTranslation();
return [
{
flavor: 'select',
icon: <SelectIcon />,
toolTip: t('Select'),
disable: false,
callback: () => {
window.dispatchEvent(
new CustomEvent('affine.switch-mouse-mode', {
detail: {
type: 'default',
},
})
);
},
},
{
flavor: 'text',
icon: <TextIcon />,
toolTip: t('Text'),
disable: true,
},
{
flavor: 'shape',
icon: <ShapeIcon />,
toolTip: t('Shape'),
disable: false,
callback: () => {
window.dispatchEvent(
new CustomEvent('affine.switch-mouse-mode', {
detail: {
type: 'shape',
color: 'black',
shape: 'rectangle',
},
})
);
},
},
{
flavor: 'sticky',
icon: <StickerIcon />,
toolTip: t('Sticky'),
disable: true,
},
{
flavor: 'pen',
icon: <PenIcon />,
toolTip: t('Pen'),
disable: true,
},
{
flavor: 'connector',
icon: <ConnectorIcon />,
toolTip: t('Connector'),
disable: true,
},
];
};
const UndoRedo = () => {
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const { currentPage } = useAppState();
const onHistoryUpdated = useHistoryUpdated();
const { t } = useTranslation();
useEffect(() => {
onHistoryUpdated(page => {
setCanUndo(page.canUndo);
setCanRedo(page.canRedo);
});
}, [onHistoryUpdated]);
return (
<StyledToolbarWrapper>
<Tooltip content={t('Undo')} placement="right-start">
<StyledToolbarItem
disable={!canUndo}
onClick={() => {
currentPage?.undo();
}}
>
<UndoIcon />
</StyledToolbarItem>
</Tooltip>
<Tooltip content={t('Redo')} placement="right-start">
<StyledToolbarItem
disable={!canRedo}
onClick={() => {
currentPage?.redo();
}}
>
<RedoIcon />
</StyledToolbarItem>
</Tooltip>
</StyledToolbarWrapper>
);
};
export const EdgelessToolbar = () => {
const { mode } = useCurrentPageMeta() || {};
return (
<MuiSlide
direction="right"
in={mode === 'edgeless'}
mountOnEnter
unmountOnExit
>
<StyledEdgelessToolbar aria-label="edgeless-toolbar">
<StyledToolbarWrapper>
{useToolbarList1().map(
({ icon, toolTip, flavor, disable, callback }, index) => {
return (
<Tooltip key={index} content={toolTip} placement="right-start">
<StyledToolbarItem
disable={disable}
onClick={() => {
console.log('click toolbar button:', flavor);
callback?.();
}}
>
{icon}
</StyledToolbarItem>
</Tooltip>
);
}
)}
</StyledToolbarWrapper>
<UndoRedo />
</StyledEdgelessToolbar>
</MuiSlide>
);
};
export default EdgelessToolbar;

View File

@@ -0,0 +1,3 @@
<svg width="36" height="35" viewBox="0 0 36 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M14.8277 12.2008C15.0955 12.4686 15.0955 12.9028 14.8277 13.1706L12.3412 15.6571H20.9551C21.6982 15.6571 22.2977 15.6571 22.7831 15.6968C23.2828 15.7376 23.7218 15.8239 24.128 16.0308C24.7731 16.3595 25.2976 16.884 25.6263 17.5292C25.8332 17.9353 25.9195 18.3743 25.9604 18.8741C26 19.3595 26 19.9589 26 20.7021V21.8286C26 22.2073 25.693 22.5143 25.3143 22.5143C24.9356 22.5143 24.6286 22.2073 24.6286 21.8286V20.7314C24.6286 19.952 24.628 19.4087 24.5935 18.9858C24.5596 18.5708 24.4964 18.3324 24.4044 18.1518C24.2071 17.7647 23.8924 17.45 23.5054 17.2528C23.3248 17.1608 23.0864 17.0976 22.6714 17.0637C22.2484 17.0291 21.7051 17.0286 20.9257 17.0286H12.3412L14.8277 19.5151C15.0955 19.7829 15.0955 20.2171 14.8277 20.4849C14.5599 20.7527 14.1258 20.7527 13.858 20.4849L10.2008 16.8277C9.93305 16.5599 9.93305 16.1258 10.2008 15.858L13.858 12.2008C14.1258 11.9331 14.5599 11.9331 14.8277 12.2008Z" fill="#9096A5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,41 @@
import { styled, displayFlex } from '@affine/component';
export const StyledEdgelessToolbar = styled.div(({ theme }) => ({
height: '320px',
position: 'absolute',
left: '12px',
top: 0,
bottom: 0,
margin: 'auto',
zIndex: theme.zIndex.modal - 1,
}));
export const StyledToolbarWrapper = styled.div(({ theme }) => ({
width: '44px',
borderRadius: '10px',
boxShadow: theme.shadow.modal,
padding: '4px',
background: theme.colors.popoverBackground,
transition: 'background .5s',
marginBottom: '12px',
}));
export const StyledToolbarItem = styled.div<{
disable?: boolean;
}>(({ theme, disable = false }) => ({
width: '36px',
height: '36px',
...displayFlex('center', 'center'),
color: disable ? theme.colors.disableColor : theme.colors.iconColor,
cursor: disable ? 'not-allowed' : 'pointer',
svg: {
width: '36px',
height: '36px',
},
':hover': disable
? {}
: {
color: theme.colors.primaryColor,
background: theme.colors.hoverBackground,
},
}));

View File

@@ -0,0 +1,85 @@
import { CSSProperties, DOMAttributes } from 'react';
type IconProps = {
style?: CSSProperties;
} & DOMAttributes<SVGElement>;
export const ArrowIcon = ({
style: propsStyle = {},
direction = 'right',
...props
}: IconProps & { direction?: 'left' | 'right' | 'middle' }) => {
const style = {
transform: `rotate(${direction === 'left' ? '0' : '180deg'})`,
opacity: direction === 'middle' ? 0 : 1,
...propsStyle,
};
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="6"
height="16"
viewBox="0 0 6 16"
fill="currentColor"
{...props}
style={style}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.602933 0.305738C0.986547 0.0865297 1.47523 0.219807 1.69444 0.603421L5.41093 7.10728C5.72715 7.66066 5.72715 8.34 5.41093 8.89338L1.69444 15.3972C1.47523 15.7809 0.986547 15.9141 0.602933 15.6949C0.219319 15.4757 0.0860414 14.987 0.305249 14.6034L4.02174 8.09956C4.05688 8.03807 4.05688 7.96259 4.02174 7.9011L0.305249 1.39724C0.0860414 1.01363 0.219319 0.524946 0.602933 0.305738Z"
/>
</svg>
);
};
export const PaperIcon = ({ style = {}, ...props }: IconProps) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="currentColor"
style={style}
{...props}
>
<path
fillRule="evenodd"
d="M17 9.8H7V8.2h10v1.6ZM12 12.8H7v-1.6h5v1.6Z"
clipRule="evenodd"
/>
<path d="m14 19 7-7h-5a2 2 0 0 0-2 2v5Z" />
<path
fillRule="evenodd"
d="M5 6.6h14c.22 0 .4.18.4.4v6.6L21 12V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h9l1.6-1.6H5a.4.4 0 0 1-.4-.4V7c0-.22.18-.4.4-.4Z"
clipRule="evenodd"
/>
</svg>
);
};
export const EdgelessIcon = ({ style = {}, ...props }: IconProps) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="currentColor"
style={style}
{...props}
>
<path
fillRule="evenodd"
d="M12 17.4a5.4 5.4 0 1 0 0-10.8 5.4 5.4 0 0 0 0 10.8Zm7-5.4a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z"
clipRule="evenodd"
/>
<path
fillRule="evenodd"
d="M18.565 8a.8.8 0 0 1 .8-.8c.797 0 1.511.07 2.07.24.5.15 1.172.477 1.334 1.202v.004c.089.405-.026.776-.186 1.065a3.165 3.165 0 0 1-.652.782c-.52.471-1.265.947-2.15 1.407-1.783.927-4.28 1.869-7.077 2.62-2.796.752-5.409 1.184-7.381 1.266-.98.04-1.848-.003-2.516-.162-.333-.079-.662-.196-.937-.38-.282-.19-.547-.48-.639-.892v-.002c-.138-.63.202-1.173.518-1.532.343-.39.836-.768 1.413-1.129a.8.8 0 0 1 .848 1.357c-.515.322-.862.605-1.06.83a1.524 1.524 0 0 0-.078.096c.07.03.169.064.304.095.461.11 1.163.158 2.08.12 1.822-.075 4.314-.481 7.033-1.212 2.718-.73 5.1-1.635 6.753-2.494.832-.433 1.441-.835 1.814-1.173.127-.115.213-.21.268-.284a1.67 1.67 0 0 0-.153-.053c-.342-.104-.878-.171-1.606-.171a.8.8 0 0 1-.8-.8Zm2.692 1.097-.004-.004a.026.026 0 0 1 .004.004Zm-18.46 5 .001-.002v.002Z"
clipRule="evenodd"
/>
</svg>
);
};

View File

@@ -0,0 +1,151 @@
import React, { useState, useEffect, cloneElement } from 'react';
import {
StyledAnimateRadioContainer,
StyledMiddleLine,
StyledRadioItem,
StyledLabel,
StyledIcon,
} from './style';
import type {
RadioItemStatus,
AnimateRadioProps,
AnimateRadioItemProps,
} from './type';
import { useTheme } from '@/providers/ThemeProvider';
import { EdgelessIcon, PaperIcon } from './Icons';
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
import { usePageHelper } from '@/hooks/use-page-helper';
import { useTranslation } from '@affine/i18n';
const PaperItem = ({ active }: { active?: boolean }) => {
const {
theme: {
colors: { iconColor, primaryColor },
},
} = useTheme();
return <PaperIcon style={{ color: active ? primaryColor : iconColor }} />;
};
const EdgelessItem = ({ active }: { active?: boolean }) => {
const {
theme: {
colors: { iconColor, primaryColor },
},
} = useTheme();
return <EdgelessIcon style={{ color: active ? primaryColor : iconColor }} />;
};
const AnimateRadioItem = ({
active,
status,
icon: propsIcon,
label,
isLeft,
...props
}: AnimateRadioItemProps) => {
const icon = (
<StyledIcon shrink={status === 'shrink'} isLeft={isLeft}>
{cloneElement(propsIcon, {
active,
})}
</StyledIcon>
);
return (
<StyledRadioItem title={label} active={active} status={status} {...props}>
{isLeft ? icon : null}
<StyledLabel shrink={status !== 'stretch'} isLeft={isLeft}>
{label}
</StyledLabel>
{isLeft ? null : icon}
</StyledRadioItem>
);
};
export const EditorModeSwitch = ({
isHover,
style = {},
}: AnimateRadioProps) => {
const { mode: themeMode } = useTheme();
const { changePageMode } = usePageHelper();
const { trash, mode = 'page', id = '' } = useCurrentPageMeta() || {};
const modifyRadioItemStatus = (): RadioItemStatus => {
return {
left: isHover
? mode === 'page'
? 'stretch'
: 'normal'
: mode === 'page'
? 'shrink'
: 'hidden',
right: isHover
? mode === 'edgeless'
? 'stretch'
: 'normal'
: mode === 'edgeless'
? 'shrink'
: 'hidden',
};
};
const [radioItemStatus, setRadioItemStatus] = useState<RadioItemStatus>(
modifyRadioItemStatus
);
useEffect(() => {
setRadioItemStatus(modifyRadioItemStatus());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isHover, mode]);
const { t } = useTranslation();
return (
<StyledAnimateRadioContainer
data-testid="editor-mode-switcher"
shrink={!isHover}
style={style}
disabled={!!trash}
>
<AnimateRadioItem
isLeft={true}
label={t('Paper')}
icon={<PaperItem />}
active={mode === 'page'}
status={radioItemStatus.left}
onClick={() => {
changePageMode(id, 'page');
}}
onMouseEnter={() => {
setRadioItemStatus({
right: 'normal',
left: 'stretch',
});
}}
onMouseLeave={() => {
setRadioItemStatus(modifyRadioItemStatus());
}}
/>
<StyledMiddleLine hidden={!isHover} dark={themeMode === 'dark'} />
<AnimateRadioItem
isLeft={false}
label={t('Edgeless')}
data-testid="switch-edgeless-item"
icon={<EdgelessItem />}
active={mode === 'edgeless'}
status={radioItemStatus.right}
onClick={() => {
changePageMode(id, 'edgeless');
}}
onMouseEnter={() => {
setRadioItemStatus({
left: 'normal',
right: 'stretch',
});
}}
onMouseLeave={() => {
setRadioItemStatus(modifyRadioItemStatus());
}}
/>
</StyledAnimateRadioContainer>
);
};
export default EditorModeSwitch;

View File

@@ -0,0 +1,155 @@
import { displayFlex, keyframes, styled } from '@affine/component';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import spring, { toString } from 'css-spring';
import type { ItemStatus } from './type';
const ANIMATE_DURATION = 500;
export const StyledAnimateRadioContainer = styled('div')<{
shrink: boolean;
disabled: boolean;
}>(({ shrink, theme, disabled }) => {
const animateScaleStretch = keyframes`${toString(
spring({ width: '36px' }, { width: '160px' }, { preset: 'gentle' })
)}`;
const animateScaleShrink = keyframes(
`${toString(
spring({ width: '160px' }, { width: '36px' }, { 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: disabled ? 'transparent' : theme.colors.hoverBackground,
position: 'relative',
display: 'flex',
transition: `background ${ANIMATE_DURATION}ms, border ${ANIMATE_DURATION}ms`,
border: '1px solid transparent',
...(disabled ? { pointerEvents: 'none' } : shrinkStyle),
':hover': {
border: disabled ? '' : `1px solid ${theme.colors.primaryColor}`,
},
};
});
export const StyledMiddleLine = styled('div')<{
hidden: boolean;
dark: boolean;
}>(({ hidden, dark }) => {
return {
width: '1px',
height: '16px',
background: dark ? '#4d4c53' : '#D0D7E3',
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: '44px' }, { width: '112px' })
)}`;
const animateScaleOrigin = keyframes(
`${toString(spring({ width: '112px' }, { width: '44px' }))}`
);
const animateScaleShrink = keyframes(
`${toString(spring({ width: '0px' }, { width: '36px' }))}`
);
const dynamicStyle =
status === 'stretch'
? {
animation: `${animateScaleStretch} ${ANIMATE_DURATION}ms forwards`,
flexShrink: '0',
}
: status === 'shrink'
? {
animation: `${animateScaleShrink} ${ANIMATE_DURATION}ms forwards`,
}
: status === 'normal'
? { animation: `${animateScaleOrigin} ${ANIMATE_DURATION}ms forwards` }
: {};
const {
colors: { iconColor, primaryColor },
} = theme;
return {
width: '0',
height: '100%',
display: 'flex',
cursor: 'pointer',
overflow: 'hidden',
color: active ? primaryColor : iconColor,
...dynamicStyle,
};
});
export const StyledLabel = styled('div')<{
shrink: boolean;
isLeft: boolean;
}>(({ shrink, isLeft }) => {
const animateScaleStretch = keyframes`${toString(
spring(
{ width: '0px' },
{ width: isLeft ? '65px' : '75px' },
{ preset: 'gentle' }
)
)}`;
const animateScaleShrink = keyframes(
`${toString(
spring(
{ width: isLeft ? '65px' : '75px' },
{ width: '0px' },
{ preset: 'gentle' }
)
)}`
);
const shrinkStyle = shrink
? {
animation: `${animateScaleShrink} ${ANIMATE_DURATION}ms forwards`,
}
: {
animation: `${animateScaleStretch} ${ANIMATE_DURATION}ms forwards`,
};
return {
display: 'flex',
alignItems: 'center',
justifyContent: isLeft ? 'flex-start' : 'flex-end',
fontSize: '16px',
flexShrink: '0',
transition: `transform ${ANIMATE_DURATION}ms`,
fontWeight: 'normal',
overflow: 'hidden',
...shrinkStyle,
};
});
export const StyledIcon = styled('div')<{
shrink: boolean;
isLeft: boolean;
}>(({ shrink, isLeft }) => {
const dynamicStyle = shrink
? { width: '36px' }
: { width: isLeft ? '44px' : '34px' };
return {
...displayFlex('center', 'center'),
flexShrink: '0',
...dynamicStyle,
};
});

View File

@@ -0,0 +1,19 @@
import { CSSProperties, DOMAttributes, ReactElement } from 'react';
export type ItemStatus = 'normal' | 'stretch' | 'shrink' | 'hidden';
export type RadioItemStatus = {
left: ItemStatus;
right: ItemStatus;
};
export type AnimateRadioProps = {
isHover: boolean;
style: CSSProperties;
};
export type AnimateRadioItemProps = {
active: boolean;
status: ItemStatus;
label: string;
icon: ReactElement;
isLeft: boolean;
} & DOMAttributes<HTMLDivElement>;

View File

@@ -0,0 +1,80 @@
import { useEffect, useRef } from 'react';
import type { Page, Workspace } from '@blocksuite/store';
import '@blocksuite/blocks';
import { EditorContainer } from '@blocksuite/editor';
import exampleMarkdown from '@/templates/Welcome-to-AFFiNE-Alpha-Downhills.md';
import { styled } from '@affine/component';
const StyledEditorContainer = styled('div')(() => {
return {
height: 'calc(100vh - 60px)',
padding: '0 32px',
};
});
type Props = {
page: Page;
workspace: Workspace;
setEditor: (editor: EditorContainer) => void;
};
export const Editor = ({ page, workspace, setEditor }: Props) => {
const editorContainer = useRef<HTMLDivElement>(null);
// const { currentWorkspace, currentPage, setEditor } = useAppState();
useEffect(() => {
let blockHubElement: HTMLElement | null = null;
const ret = () => {
const node = editorContainer.current;
while (node?.firstChild) {
node.removeChild(node.firstChild);
}
blockHubElement?.remove();
};
const editor = new EditorContainer();
editor.page = page;
editor.createBlockHub().then(blockHub => {
const toolWrapper = document.querySelector('#toolWrapper');
if (!toolWrapper) {
// In an invitation page there is no toolWrapper, which contains helper icon and blockHub icon
return;
}
blockHubElement = blockHub;
toolWrapper.appendChild(blockHub);
});
editorContainer.current?.appendChild(editor);
if (page.isEmpty) {
const isFirstPage = workspace?.meta.pageMetas.length === 1;
// Can not use useCurrentPageMeta to get new title, cause meta title will trigger rerender, but the second time can not remove title
const { title: metaTitle } = page.meta;
const title = metaTitle
? metaTitle
: isFirstPage
? 'Welcome to AFFiNE Alpha "Downhills"'
: '';
workspace?.setPageMeta(page.id, { title });
const pageBlockId = page.addBlockByFlavour('affine:page', { title });
page.addBlockByFlavour('affine:surface', {}, null);
// Add frame block inside page block
const frameId = page.addBlockByFlavour('affine:frame', {}, pageBlockId);
// Add paragraph block inside frame block
// If this is a first page in workspace, init an introduction markdown
if (isFirstPage) {
editor.clipboard.importMarkdown(exampleMarkdown, frameId);
workspace.setPageMeta(page.id, { title });
page.resetHistory();
} else {
page.addBlockByFlavour('affine:paragraph', {}, frameId);
}
page.resetHistory();
}
setEditor(editor);
return ret;
}, [workspace, page, setEditor]);
return <StyledEditorContainer ref={editorContainer} />;
};
export default Editor;

View File

@@ -0,0 +1,111 @@
import { styled } from '@affine/component';
import { Modal, ModalWrapper } from '@affine/component';
import { Button, IconButton } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { useAppState } from '@/providers/app-state-provider';
import { useState } from 'react';
import router from 'next/router';
import { toast } from '@affine/component';
import { CloseIcon } from '@blocksuite/icons';
interface EnableWorkspaceModalProps {
open: boolean;
onClose: () => void;
}
export const EnableWorkspaceModal = ({
open,
onClose,
}: EnableWorkspaceModalProps) => {
const { t } = useTranslation();
const { user, dataCenter, login, currentWorkspace } = useAppState();
const [loading, setLoading] = useState(false);
return (
<Modal open={open} onClose={onClose} data-testid="logout-modal">
<ModalWrapper width={560} height={292}>
<Header>
<IconButton
onClick={() => {
onClose();
}}
>
<CloseIcon />
</IconButton>
</Header>
<Content>
<ContentTitle>{t('Enable AFFiNE Cloud')}?</ContentTitle>
<StyleTips>{t('Enable AFFiNE Cloud Description')}</StyleTips>
{/* <StyleTips>{t('Retain local cached data')}</StyleTips> */}
<div>
<StyleButton
shape="round"
type="primary"
loading={loading}
onClick={async () => {
setLoading(true);
if (!user) {
await login();
}
if (currentWorkspace) {
const workspace = await dataCenter.enableWorkspaceCloud(
currentWorkspace
);
workspace &&
router.push(`/workspace/${workspace.id}/setting`);
toast(t('Enabled success'));
}
}}
>
{user ? t('Enable') : t('Sign in and Enable')}
</StyleButton>
<StyleButton
shape="round"
onClick={() => {
onClose();
}}
>
{t('Not now')}
</StyleButton>
</div>
</Content>
</ModalWrapper>
</Modal>
);
};
const Header = styled('div')({
height: '44px',
display: 'flex',
flexDirection: 'row-reverse',
paddingRight: '10px',
paddingTop: '10px',
});
const Content = styled('div')({
textAlign: 'center',
});
const ContentTitle = styled('h1')({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'center',
});
const StyleTips = styled('div')(() => {
return {
userSelect: 'none',
width: '400px',
margin: 'auto',
marginBottom: '32px',
marginTop: '12px',
};
});
const StyleButton = styled(Button)(() => {
return {
width: '284px',
display: 'block',
margin: 'auto',
marginTop: '16px',
};
});

View File

@@ -0,0 +1,28 @@
import { Button } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { useState } from 'react';
import { EnableWorkspaceModal } from './EnableWorkspaceModal';
export const EnableWorkspaceButton = () => {
const { t } = useTranslation();
const [enableModalOpen, setEnableModalOpen] = useState(false);
return (
<>
<Button
type="light"
shape="circle"
onClick={async () => {
setEnableModalOpen(true);
}}
>
{t('Enable AFFiNE Cloud')}
</Button>
<EnableWorkspaceModal
open={enableModalOpen}
onClose={() => {
setEnableModalOpen(false);
}}
></EnableWorkspaceModal>
</>
);
};

View File

@@ -0,0 +1,49 @@
import { Button } from '@affine/component';
import { FC, useRef, ChangeEvent, ReactElement } from 'react';
import { styled } from '@affine/component';
import { useTranslation } from '@affine/i18n';
interface Props {
uploadType?: string;
children?: ReactElement;
accept?: string;
fileChange: (file: File) => void;
}
export const Upload: FC<Props> = props => {
const { fileChange, accept } = props;
const { t } = useTranslation();
const input_ref = useRef<HTMLInputElement>(null);
const _chooseFile = () => {
if (input_ref.current) {
input_ref.current.click();
}
};
const _handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files) {
return;
}
const file = files[0];
fileChange(file);
if (input_ref.current) {
input_ref.current.value = '';
}
};
return (
<UploadStyle onClick={_chooseFile}>
{props.children ?? <Button>{t('Upload')}</Button>}
<input
ref={input_ref}
type="file"
style={{ display: 'none' }}
onChange={_handleInputChange}
accept={accept}
/>
</UploadStyle>
);
};
const UploadStyle = styled('div')(() => {
return {
display: 'inline-block',
};
});

View File

@@ -0,0 +1,78 @@
import React, { useEffect, useState } from 'react';
import {
StyledSearchArrowWrapper,
StyledSwitchWrapper,
StyledTitle,
StyledTitleWrapper,
} from './styles';
import { Content } from '@affine/component';
import { useAppState } from '@/providers/app-state-provider';
import EditorModeSwitch from '@/components/editor-mode-switch';
import QuickSearchButton from './QuickSearchButton';
import Header from './Header';
import usePropsUpdated from '@/hooks/use-props-updated';
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
export const EditorHeader = () => {
const [title, setTitle] = useState('');
const [isHover, setIsHover] = useState(false);
const { editor } = useAppState();
const { trash: isTrash = false } = useCurrentPageMeta() || {};
const onPropsUpdated = usePropsUpdated();
useEffect(() => {
onPropsUpdated(editor => {
setTitle(editor.pageBlockModel?.title || 'Untitled');
});
}, [onPropsUpdated]);
useEffect(() => {
setTimeout(() => {
// If first time in, need to wait for editor to be inserted into DOM
setTitle(editor?.pageBlockModel?.title || 'Untitled');
}, 300);
}, [editor]);
return (
<Header
rightItems={
isTrash
? ['trashButtonGroup']
: ['syncUser', 'themeModeSwitch', 'editorOptionMenu']
}
>
{title && (
<StyledTitle
data-tauri-drag-region
onMouseEnter={() => {
if (isTrash) return;
setIsHover(true);
}}
onMouseLeave={() => {
if (isTrash) return;
setIsHover(false);
}}
>
<StyledTitleWrapper>
<StyledSwitchWrapper>
<EditorModeSwitch
isHover={isHover}
style={{
marginRight: '12px',
}}
/>
</StyledSwitchWrapper>
<Content ellipsis={true}>{title}</Content>
<StyledSearchArrowWrapper>
<QuickSearchButton />
</StyledSearchArrowWrapper>
</StyledTitleWrapper>
</StyledTitle>
)}
</Header>
);
};
export default EditorHeader;

View File

@@ -0,0 +1,75 @@
import React, { PropsWithChildren, ReactNode, useState } from 'react';
import {
StyledHeader,
StyledHeaderRightSide,
StyledHeaderContainer,
StyledBrowserWarning,
StyledCloseButton,
} from './styles';
import { CloseIcon } from '@blocksuite/icons';
import { useWarningMessage, shouldShowWarning } from './utils';
import EditorOptionMenu from './header-right-items/EditorOptionMenu';
import TrashButtonGroup from './header-right-items/TrashButtonGroup';
import ThemeModeSwitch from './header-right-items/theme-mode-switch';
import SyncUser from './header-right-items/SyncUser';
const BrowserWarning = ({
show,
onClose,
}: {
show: boolean;
onClose: () => void;
}) => {
return (
<StyledBrowserWarning show={show}>
{useWarningMessage()}
<StyledCloseButton onClick={onClose}>
<CloseIcon />
</StyledCloseButton>
</StyledBrowserWarning>
);
};
type HeaderRightItemNames =
| 'editorOptionMenu'
| 'trashButtonGroup'
| 'themeModeSwitch'
| 'syncUser';
const HeaderRightItems: Record<HeaderRightItemNames, ReactNode> = {
editorOptionMenu: <EditorOptionMenu key="editorOptionMenu" />,
trashButtonGroup: <TrashButtonGroup key="trashButtonGroup" />,
themeModeSwitch: <ThemeModeSwitch key="themeModeSwitch" />,
syncUser: <SyncUser key="syncUser" />,
};
export const Header = ({
rightItems = ['syncUser', 'themeModeSwitch'],
children,
}: PropsWithChildren<{ rightItems?: HeaderRightItemNames[] }>) => {
const [showWarning, setShowWarning] = useState(shouldShowWarning());
return (
<StyledHeaderContainer hasWarning={showWarning}>
<BrowserWarning
show={showWarning}
onClose={() => {
setShowWarning(false);
}}
/>
<StyledHeader
hasWarning={showWarning}
data-testid="editor-header-items"
data-tauri-drag-region
>
{children}
<StyledHeaderRightSide>
{rightItems.map(itemName => {
return HeaderRightItems[itemName];
})}
</StyledHeaderRightSide>
</StyledHeader>
</StyledHeaderContainer>
);
};
export default Header;

View File

@@ -0,0 +1,22 @@
import { PropsWithChildren, ReactNode } from 'react';
import Header from './Header';
import QuickSearchButton from './QuickSearchButton';
import { StyledPageListTittleWrapper } from './styles';
// import QuickSearchButton from './QuickSearchButton';
export type PageListHeaderProps = PropsWithChildren<{
icon?: ReactNode;
}>;
export const PageListHeader = ({ icon, children }: PageListHeaderProps) => {
return (
<Header>
<StyledPageListTittleWrapper>
{icon}
{children}
<QuickSearchButton />
</StyledPageListTittleWrapper>
</Header>
);
};
export default PageListHeader;

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { IconButton, IconButtonProps } from '@affine/component';
import { ArrowDownIcon } from '@blocksuite/icons';
import { useModal } from '@/store/globalModal';
import { styled } from '@affine/component';
const StyledIconButtonWithAnimate = styled(IconButton)(({ theme }) => {
return {
svg: {
transition: 'transform 0.15s ease-in-out',
},
':hover': {
svg: {
transform: 'translateY(3px)',
},
'::after': {
background: theme.colors.pageBackground,
},
},
};
});
export const QuickSearchButton = ({
onClick,
...props
}: Omit<IconButtonProps, 'children'>) => {
const { triggerQuickSearchModal } = useModal();
return (
<StyledIconButtonWithAnimate
data-testid="header-quickSearchButton"
{...props}
onClick={e => {
onClick?.(e);
triggerQuickSearchModal();
}}
>
<ArrowDownIcon />
</StyledIconButtonWithAnimate>
);
};
export default QuickSearchButton;

View File

@@ -0,0 +1,117 @@
import { Menu, MenuItem } from '@affine/component';
import { IconButton } from '@affine/component';
import {
EdgelessIcon,
ExportIcon,
ExportToHtmlIcon,
ExportToMarkdownIcon,
FavouritedIcon,
FavouritesIcon,
MoreVerticalIcon,
PaperIcon,
TrashIcon,
} from '@blocksuite/icons';
import { useAppState } from '@/providers/app-state-provider';
import { usePageHelper } from '@/hooks/use-page-helper';
import { useConfirm } from '@/providers/ConfirmProvider';
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
import { toast } from '@affine/component';
import { useTranslation } from '@affine/i18n';
const PopoverContent = () => {
const { editor } = useAppState();
const { toggleFavoritePage, toggleDeletePage } = usePageHelper();
const { changePageMode } = usePageHelper();
const { confirm } = useConfirm();
const { t } = useTranslation();
const {
mode = 'page',
id = '',
favorite = false,
title = '',
} = useCurrentPageMeta() || {};
return (
<>
<MenuItem
data-testid="editor-option-menu-favorite"
onClick={() => {
toggleFavoritePage(id);
toast(
favorite ? t('Removed from Favorites') : t('Added to Favorites')
);
}}
icon={favorite ? <FavouritedIcon /> : <FavouritesIcon />}
>
{favorite ? t('Remove from favorites') : t('Add to favorites')}
</MenuItem>
<MenuItem
icon={mode === 'page' ? <EdgelessIcon /> : <PaperIcon />}
data-testid="editor-option-menu-edgeless"
onClick={() => {
changePageMode(id, mode === 'page' ? 'edgeless' : 'page');
}}
>
{t('Convert to ')}
{mode === 'page' ? t('Edgeless') : t('Page')}
</MenuItem>
<Menu
placement="left-start"
content={
<>
<MenuItem
onClick={() => {
editor && editor.contentParser.onExportHtml();
}}
icon={<ExportToHtmlIcon />}
>
{t('Export to HTML')}
</MenuItem>
<MenuItem
onClick={() => {
editor && editor.contentParser.onExportMarkdown();
}}
icon={<ExportToMarkdownIcon />}
>
{t('Export to Markdown')}
</MenuItem>
</>
}
>
<MenuItem icon={<ExportIcon />} isDir={true}>
{t('Export')}
</MenuItem>
</Menu>
<MenuItem
data-testid="editor-option-menu-delete"
onClick={() => {
confirm({
title: t('Delete page?'),
content: t('will be moved to Trash', {
title: title || 'Untitled',
}),
confirmText: t('Delete'),
confirmType: 'danger',
}).then(confirm => {
confirm && toggleDeletePage(id);
confirm && toast(t('Moved to Trash'));
});
}}
icon={<TrashIcon />}
>
{t('Delete')}
</MenuItem>
</>
);
};
export const EditorOptionMenu = () => {
return (
<Menu content={<PopoverContent />} placement="bottom-end">
<IconButton>
<MoreVerticalIcon />
</IconButton>
</Menu>
);
};
export default EditorOptionMenu;

View File

@@ -0,0 +1,51 @@
import { CloudUnsyncedIcon } from '@blocksuite/icons';
import { useModal } from '@/store/globalModal';
import { useAppState } from '@/providers/app-state-provider';
import { IconButton } from '@affine/component';
// Temporary solution to use this component, since the @blocksuite/icons has not been published yet
const DefaultSyncIcon = () => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 13.6493C3 16.6044 5.41766 19 8.4 19L16.5 19C18.9853 19 21 16.9839 21 14.4969C21 12.6503 19.8893 10.9449 18.3 10.25C18.1317 7.32251 15.684 5 12.6893 5C10.3514 5 8.34694 6.48637 7.5 8.5C4.8 8.9375 3 11.2001 3 13.6493Z"
stroke="#888A9E"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11.4571 9L9 16H10.4392L12.0021 11.1586L13.5657 16H15L12.5425 9H11.4571Z"
fill="#888A9E"
/>
</svg>
);
};
export const SyncUser = () => {
const { triggerLoginModal } = useModal();
const appState = useAppState();
return appState.user ? (
<IconButton iconSize="middle" disabled>
<DefaultSyncIcon />
</IconButton>
) : (
<IconButton
iconSize="middle"
data-testid="cloud-unsync-icon"
onClick={triggerLoginModal}
>
<CloudUnsyncedIcon />
</IconButton>
);
};
export default SyncUser;

View File

@@ -0,0 +1,53 @@
import { Button } from '@affine/component';
import { usePageHelper } from '@/hooks/use-page-helper';
import { useAppState } from '@/providers/app-state-provider';
import { useConfirm } from '@/providers/ConfirmProvider';
import { useRouter } from 'next/router';
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
import { useTranslation } from '@affine/i18n';
export const TrashButtonGroup = () => {
const { permanentlyDeletePage } = usePageHelper();
const { currentWorkspace } = useAppState();
const { toggleDeletePage } = usePageHelper();
const { confirm } = useConfirm();
const router = useRouter();
const { id = '' } = useCurrentPageMeta() || {};
const { t } = useTranslation();
return (
<>
<Button
bold={true}
shape="round"
style={{ marginRight: '24px' }}
onClick={() => {
toggleDeletePage(id);
}}
>
{t('Restore it')}
</Button>
<Button
bold={true}
shape="round"
type="danger"
onClick={() => {
confirm({
title: t('TrashButtonGroupTitle'),
content: t('TrashButtonGroupDescription'),
confirmText: t('Delete'),
confirmType: 'danger',
}).then(confirm => {
if (confirm) {
router.push(`/workspace/${currentWorkspace?.id}/all`);
permanentlyDeletePage(id);
}
});
}}
>
{t('Delete permanently')}
</Button>
</>
);
};
export default TrashButtonGroup;

View File

@@ -0,0 +1,44 @@
import type { DOMAttributes, CSSProperties } from 'react';
type IconProps = {
style?: CSSProperties;
} & DOMAttributes<SVGElement>;
export const MoonIcon = ({ style = {}, ...props }: IconProps) => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
style={style}
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.54893 3.31407C9.33328 3.08158 9.27962 2.74521 9.41255 2.45912C9.54547 2.17302 9.83936 1.99233 10.1595 1.99986C13.4456 2.07712 16.5114 4.08044 17.7359 7.29071C19.3437 11.5057 17.1672 16.2024 12.8744 17.781C9.60251 18.9843 6.04745 18.0285 3.82974 15.6428C3.61375 15.4104 3.55978 15.0739 3.69257 14.7876C3.82537 14.5014 4.11931 14.3205 4.43962 14.3279C5.27228 14.3474 6.12412 14.2171 6.94979 13.9135C10.415 12.6391 12.172 8.84782 10.8741 5.44537C10.5657 4.63692 10.1061 3.91474 9.54893 3.31407Z"
/>
</svg>
);
};
export const SunIcon = ({ style = {}, ...props }: IconProps) => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
style={style}
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.8002 2.5002C10.8002 2.05837 10.442 1.7002 10.0002 1.7002C9.55837 1.7002 9.2002 2.05837 9.2002 2.5002V3.33353C9.2002 3.77536 9.55837 4.13353 10.0002 4.13353C10.442 4.13353 10.8002 3.77536 10.8002 3.33353V2.5002ZM5.14921 4.01784C4.83679 3.70542 4.33026 3.70542 4.01784 4.01784C3.70542 4.33026 3.70542 4.83679 4.01784 5.14921L4.69627 5.82764C5.00869 6.14006 5.51522 6.14006 5.82764 5.82764C6.14006 5.51522 6.14006 5.00869 5.82764 4.69627L5.14921 4.01784ZM15.9825 5.1492C16.2949 4.83678 16.2949 4.33025 15.9825 4.01783C15.6701 3.70542 15.1636 3.70543 14.8511 4.01785L14.1727 4.69628C13.8603 5.00871 13.8603 5.51524 14.1728 5.82765C14.4852 6.14007 14.9917 6.14006 15.3041 5.82763L15.9825 5.1492ZM10.0002 5.86686C7.71742 5.86686 5.86686 7.71742 5.86686 10.0002C5.86686 12.283 7.71742 14.1335 10.0002 14.1335C12.283 14.1335 14.1335 12.283 14.1335 10.0002C14.1335 7.71742 12.283 5.86686 10.0002 5.86686ZM2.5002 9.2002C2.05837 9.2002 1.7002 9.55837 1.7002 10.0002C1.7002 10.442 2.05837 10.8002 2.5002 10.8002H3.33353C3.77536 10.8002 4.13353 10.442 4.13353 10.0002C4.13353 9.55837 3.77536 9.2002 3.33353 9.2002H2.5002ZM16.6669 9.2002C16.225 9.2002 15.8669 9.55837 15.8669 10.0002C15.8669 10.442 16.225 10.8002 16.6669 10.8002H17.5002C17.942 10.8002 18.3002 10.442 18.3002 10.0002C18.3002 9.55837 17.942 9.2002 17.5002 9.2002H16.6669ZM5.82623 15.309C6.13943 14.9973 6.14069 14.4908 5.82906 14.1776C5.51742 13.8644 5.01089 13.8631 4.69769 14.1748L4.01926 14.8498C3.70606 15.1615 3.70479 15.668 4.01643 15.9812C4.32807 16.2944 4.8346 16.2956 5.1478 15.984L5.82623 15.309ZM15.3027 14.1748C14.9895 13.8631 14.483 13.8644 14.1713 14.1776C13.8597 14.4908 13.861 14.9973 14.1742 15.3089L14.8526 15.984C15.1658 16.2956 15.6723 16.2944 15.9839 15.9812C16.2956 15.668 16.2943 15.1615 15.9811 14.8498L15.3027 14.1748ZM10.8002 16.6669C10.8002 16.225 10.442 15.8669 10.0002 15.8669C9.55837 15.8669 9.2002 16.225 9.2002 16.6669V17.5002C9.2002 17.942 9.55837 18.3002 10.0002 18.3002C10.442 18.3002 10.8002 17.942 10.8002 17.5002V16.6669Z"
/>
</svg>
);
};

View File

@@ -0,0 +1,49 @@
import { useState } from 'react';
import { useTheme } from '@/providers/ThemeProvider';
import { MoonIcon, SunIcon } from './Icons';
import { StyledThemeModeSwitch, StyledSwitchItem } from './style';
export const ThemeModeSwitch = () => {
const { mode, changeMode } = useTheme();
const [isHover, setIsHover] = useState(false);
const [firstTrigger, setFirstTrigger] = useState(false);
return (
<StyledThemeModeSwitch
data-testid="change-theme-container"
onMouseEnter={() => {
setIsHover(true);
if (!firstTrigger) {
setFirstTrigger(true);
}
}}
onMouseLeave={() => {
setIsHover(false);
}}
>
<StyledSwitchItem
data-testid="change-theme-light"
active={mode === 'light'}
isHover={isHover}
firstTrigger={firstTrigger}
onClick={() => {
changeMode('light');
}}
>
<SunIcon />
</StyledSwitchItem>
<StyledSwitchItem
data-testid="change-theme-dark"
active={mode === 'dark'}
isHover={isHover}
firstTrigger={firstTrigger}
onClick={() => {
changeMode('dark');
}}
>
<MoonIcon />
</StyledSwitchItem>
</StyledThemeModeSwitch>
);
};
export default ThemeModeSwitch;

View File

@@ -0,0 +1,66 @@
import { displayFlex, keyframes, styled } from '@affine/component';
import { CSSProperties } from 'react';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import spring, { toString } from 'css-spring';
const ANIMATE_DURATION = 400;
export const StyledThemeModeSwitch = styled('div')({
width: '32px',
height: '32px',
borderRadius: '5px',
overflow: 'hidden',
backgroundColor: 'transparent',
position: 'relative',
});
export const StyledSwitchItem = styled('div')<{
active: boolean;
isHover: boolean;
firstTrigger: boolean;
}>(({ active, isHover, firstTrigger, theme }) => {
const activeRaiseAnimate = keyframes`${toString(
spring({ top: '0' }, { top: '-100%' }, { preset: 'gentle' })
)}`;
const raiseAnimate = keyframes`${toString(
spring({ top: '100%' }, { top: '0' }, { preset: 'gentle' })
)}`;
const activeDeclineAnimate = keyframes`${toString(
spring({ top: '-100%' }, { top: '0' }, { preset: 'gentle' })
)}`;
const declineAnimate = keyframes`${toString(
spring({ top: '0' }, { top: '100%' }, { preset: 'gentle' })
)}`;
const activeStyle = active
? {
color: theme.colors.iconColor,
top: '0',
animation: firstTrigger
? `${
isHover ? activeRaiseAnimate : activeDeclineAnimate
} ${ANIMATE_DURATION}ms forwards`
: 'unset',
animationDirection: isHover ? 'normal' : 'alternate',
}
: ({
top: '100%',
color: theme.colors.primaryColor,
backgroundColor: theme.colors.hoverBackground,
animation: firstTrigger
? `${
isHover ? raiseAnimate : declineAnimate
} ${ANIMATE_DURATION}ms forwards`
: 'unset',
animationDirection: isHover ? 'normal' : 'alternate',
} as CSSProperties);
return {
width: '32px',
height: '32px',
position: 'absolute',
left: '0',
...displayFlex('center', 'center'),
cursor: 'pointer',
...activeStyle,
};
});

View File

@@ -0,0 +1,3 @@
export * from './Header';
export * from './EditorHeader';
export * from './PageListHeader';

View File

@@ -0,0 +1,113 @@
import { displayFlex, styled } from '@affine/component';
export const StyledHeaderContainer = styled.div<{ hasWarning: boolean }>(
({ hasWarning }) => {
return {
height: hasWarning ? '96px' : '60px',
};
}
);
export const StyledHeader = styled.div<{ hasWarning: boolean }>(() => {
return {
height: '60px',
width: '100%',
...displayFlex('flex-end', 'center'),
background: 'var(--affine-page-background)',
transition: 'background-color 0.5s',
zIndex: 99,
};
});
export const StyledTitle = styled('div')(({ theme }) => ({
width: '720px',
height: '100%',
margin: 'auto',
...displayFlex('center', 'center'),
fontSize: theme.font.base,
}));
export const StyledTitleWrapper = styled('div')({
maxWidth: '720px',
height: '100%',
position: 'relative',
...displayFlex('center', 'center'),
});
export const StyledHeaderRightSide = styled('div')({
height: '100%',
display: 'flex',
alignItems: 'center',
});
export const StyledBrowserWarning = styled.div<{ show: boolean }>(
({ theme, show }) => {
return {
backgroundColor: theme.colors.warningBackground,
color: theme.colors.warningColor,
height: '36px',
width: '100vw',
fontSize: theme.font.sm,
position: 'fixed',
left: '0',
top: '0',
display: show ? 'flex' : 'none',
justifyContent: 'center',
alignItems: 'center',
};
}
);
export const StyledCloseButton = styled.div(({ theme }) => {
return {
width: '36px',
height: '36px',
color: theme.colors.iconColor,
cursor: 'pointer',
...displayFlex('center', 'center'),
position: 'absolute',
right: '15px',
top: '0',
svg: {
width: '15px',
height: '15px',
position: 'relative',
zIndex: 1,
},
};
});
export const StyledSwitchWrapper = styled.div(() => {
return {
position: 'absolute',
right: '100%',
top: 0,
bottom: 0,
margin: 'auto',
...displayFlex('center', 'center'),
};
});
export const StyledSearchArrowWrapper = styled.div(() => {
return {
position: 'absolute',
left: 'calc(100% + 4px)',
top: 0,
bottom: 0,
margin: 'auto',
...displayFlex('center', 'center'),
};
});
export const StyledPageListTittleWrapper = styled(StyledTitle)(({ theme }) => {
return {
fontSize: theme.font.base,
color: theme.colors.textColor,
'>svg': {
fontSize: '20px',
marginRight: '12px',
},
};
});

View File

@@ -0,0 +1,39 @@
import getIsMobile from '@/utils/get-is-mobile';
import { Trans, useTranslation } from '@affine/i18n';
// Inspire by https://stackoverflow.com/a/4900484/8415727
const getChromeVersion = () => {
const raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
return raw ? parseInt(raw[2], 10) : false;
};
const getIsChrome = () => {
return (
/Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor)
);
};
const minimumChromeVersion = 102;
export const shouldShowWarning = () => {
return (
!window.CLIENT_APP &&
!getIsMobile() &&
(!getIsChrome() || getChromeVersion() < minimumChromeVersion)
);
};
export const useWarningMessage = () => {
const { t } = useTranslation();
if (!getIsChrome()) {
return (
<span>
<Trans i18nKey="recommendBrowser">
We recommend the <strong>Chrome</strong> browser for optimal
experience.
</Trans>
</span>
);
}
if (getChromeVersion() < minimumChromeVersion) {
return <span>{t('upgradeBrowser')}</span>;
}
return '';
};

View File

@@ -0,0 +1,59 @@
export const HelpIcon = () => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12.0041 3.8002C7.47536 3.8002 3.8041 7.47146 3.8041 12.0002C3.8041 13.311 4.1111 14.5479 4.65639 15.6449C4.92133 16.1779 4.91348 16.8471 4.85228 17.3998C4.7869 17.9901 4.63749 18.6238 4.47572 19.1908C4.3722 19.5537 4.26086 19.8987 4.1573 20.2002H12.0041C16.5328 20.2002 20.2041 16.5289 20.2041 12.0002C20.2041 7.47146 16.5328 3.8002 12.0041 3.8002ZM2.26631 20.6926L2.2668 20.6914L2.26948 20.685L2.28104 20.6567C2.29139 20.6312 2.30687 20.5928 2.32657 20.5429C2.36599 20.4431 2.42218 20.2979 2.48795 20.1192C2.61992 19.7607 2.78833 19.2734 2.93712 18.7519C3.08715 18.226 3.21054 17.6884 3.262 17.2236C3.31765 16.7212 3.2707 16.4518 3.22364 16.3571C2.57081 15.0438 2.2041 13.5637 2.2041 12.0002C2.2041 6.58781 6.59171 2.2002 12.0041 2.2002C17.4165 2.2002 21.8041 6.5878 21.8041 12.0002C21.8041 17.4126 17.4165 21.8002 12.0041 21.8002H3.0049C2.73745 21.8002 2.4876 21.6665 2.33922 21.444C2.19087 21.2215 2.16356 20.9395 2.26631 20.6926ZM11.9672 9.0502C11.4091 9.0502 10.9382 9.43186 10.8049 9.9496C10.6948 10.3775 10.2587 10.6351 9.83079 10.5249C9.40291 10.4148 9.14532 9.97867 9.25545 9.55079C9.56623 8.3433 10.6614 7.4502 11.9672 7.4502C13.5136 7.4502 14.7672 8.7038 14.7672 10.2502C14.7672 11.1058 14.3536 11.6751 13.8978 12.115C13.7108 12.2955 13.4978 12.4721 13.2997 12.6362C13.2705 12.6604 13.2416 12.6844 13.2132 12.7081C12.982 12.9004 12.7556 13.0932 12.5329 13.3159C12.2205 13.6283 11.7139 13.6283 11.4015 13.3159C11.0891 13.0035 11.0891 12.4969 11.4015 12.1845C11.6788 11.9072 11.9525 11.6756 12.19 11.478C12.2213 11.4519 12.2517 11.4267 12.2812 11.4022C12.4849 11.2332 12.6465 11.0991 12.7866 10.9638C13.0808 10.6799 13.1672 10.4992 13.1672 10.2502C13.1672 9.58745 12.6299 9.0502 11.9672 9.0502ZM11.9772 16.5502H11.9672C11.5254 16.5502 11.1672 16.192 11.1672 15.7502C11.1672 15.3084 11.5254 14.9502 11.9672 14.9502H11.9772C12.419 14.9502 12.7772 15.3084 12.7772 15.7502C12.7772 16.192 12.419 16.5502 11.9772 16.5502Z"
/>
</svg>
);
};
export const ContactIcon = () => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M3.8 11C3.8 6.47126 7.47126 2.8 12 2.8V1.2C6.58761 1.2 2.2 5.58761 2.2 11H3.8ZM12 2.8C16.5287 2.8 20.2 6.47126 20.2 11H21.8C21.8 5.58761 17.4124 1.2 12 1.2V2.8ZM3 11.8H5V10.2H3V11.8ZM6.2 13V15H7.8V13H6.2ZM5 16.2H4V17.8H5V16.2ZM4 16.2C3.88954 16.2 3.8 16.1105 3.8 16H2.2C2.2 16.9941 3.00589 17.8 4 17.8V16.2ZM6.2 15C6.2 15.6627 5.66274 16.2 5 16.2V17.8C6.5464 17.8 7.8 16.5464 7.8 15H6.2ZM5 11.8C5.66274 11.8 6.2 12.3373 6.2 13H7.8C7.8 11.4536 6.5464 10.2 5 10.2V11.8ZM21 10.2H19V11.8H21V10.2ZM16.2 13V15H17.8V13H16.2ZM19 17.8H20V16.2H19V17.8ZM20 17.8C20.9941 17.8 21.8 16.9941 21.8 16H20.2C20.2 16.1105 20.1105 16.2 20 16.2V17.8ZM16.2 15C16.2 16.5464 17.4536 17.8 19 17.8V16.2C18.3373 16.2 17.8 15.6627 17.8 15H16.2ZM19 10.2C17.4536 10.2 16.2 11.4536 16.2 13H17.8C17.8 12.3373 18.3373 11.8 19 11.8V10.2ZM4.2 17V18H5.8V17H4.2ZM7 20.8H9V19.2H7V20.8ZM4.2 18C4.2 19.5464 5.4536 20.8 7 20.8V19.2C6.33726 19.2 5.8 18.6627 5.8 18H4.2ZM10.5 19.3H11.5V17.7H10.5V19.3ZM11.5 20.7H10.5V22.3H11.5V20.7ZM10.5 20.7C10.1134 20.7 9.8 20.3866 9.8 20H8.2C8.2 21.2703 9.22974 22.3 10.5 22.3V20.7ZM12.2 20C12.2 20.3866 11.8866 20.7 11.5 20.7V22.3C12.7703 22.3 13.8 21.2703 13.8 20H12.2ZM11.5 19.3C11.8866 19.3 12.2 19.6134 12.2 20H13.8C13.8 18.7297 12.7703 17.7 11.5 17.7V19.3ZM10.5 17.7C9.22974 17.7 8.2 18.7297 8.2 20H9.8C9.8 19.6134 10.1134 19.3 10.5 19.3V17.7ZM2.2 11V16H3.8V11H2.2ZM20.2 11V16H21.8V11H20.2Z" />
</svg>
);
};
export const KeyboardIcon = () => {
return (
<svg
width="24"
height="25"
viewBox="0 0 24 25"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M19.745 5C20.3417 5 20.914 5.23705 21.336 5.65901C21.7579 6.08097 21.995 6.65326 21.995 7.25V16.755C21.995 17.3517 21.7579 17.924 21.336 18.346C20.914 18.7679 20.3417 19.005 19.745 19.005H4.25C3.95453 19.005 3.66194 18.9468 3.38896 18.8337C3.11598 18.7207 2.86794 18.5549 2.65901 18.346C2.45008 18.1371 2.28434 17.889 2.17127 17.616C2.0582 17.3431 2 17.0505 2 16.755V7.25C2 6.65326 2.23705 6.08097 2.65901 5.65901C3.08097 5.23705 3.65326 5 4.25 5H19.745ZM19.745 6.5H4.25C4.05109 6.5 3.86032 6.57902 3.71967 6.71967C3.57902 6.86032 3.5 7.05109 3.5 7.25V16.755C3.5 17.169 3.836 17.505 4.25 17.505H19.745C19.9439 17.505 20.1347 17.426 20.2753 17.2853C20.416 17.1447 20.495 16.9539 20.495 16.755V7.25C20.495 7.05109 20.416 6.86032 20.2753 6.71967C20.1347 6.57902 19.9439 6.5 19.745 6.5ZM6.75 14.5H17.25C17.44 14.5001 17.6229 14.5722 17.7618 14.702C17.9006 14.8317 17.9851 15.0093 17.998 15.1989C18.011 15.3885 17.9515 15.5759 17.8316 15.7233C17.7117 15.8707 17.5402 15.9671 17.352 15.993L17.25 16H6.75C6.55998 15.9999 6.37706 15.9278 6.23821 15.798C6.09936 15.6683 6.01493 15.4907 6.00197 15.3011C5.98902 15.1115 6.04852 14.9241 6.16843 14.7767C6.28835 14.6293 6.45975 14.5329 6.648 14.507L6.75 14.5H17.25H6.75ZM16.5 11C16.7652 11 17.0196 11.1054 17.2071 11.2929C17.3946 11.4804 17.5 11.7348 17.5 12C17.5 12.2652 17.3946 12.5196 17.2071 12.7071C17.0196 12.8946 16.7652 13 16.5 13C16.2348 13 15.9804 12.8946 15.7929 12.7071C15.6054 12.5196 15.5 12.2652 15.5 12C15.5 11.7348 15.6054 11.4804 15.7929 11.2929C15.9804 11.1054 16.2348 11 16.5 11ZM10.505 11C10.7702 11 11.0246 11.1054 11.2121 11.2929C11.3996 11.4804 11.505 11.7348 11.505 12C11.505 12.2652 11.3996 12.5196 11.2121 12.7071C11.0246 12.8946 10.7702 13 10.505 13C10.2398 13 9.98543 12.8946 9.79789 12.7071C9.61036 12.5196 9.505 12.2652 9.505 12C9.505 11.7348 9.61036 11.4804 9.79789 11.2929C9.98543 11.1054 10.2398 11 10.505 11ZM7.505 11C7.77022 11 8.02457 11.1054 8.21211 11.2929C8.39964 11.4804 8.505 11.7348 8.505 12C8.505 12.2652 8.39964 12.5196 8.21211 12.7071C8.02457 12.8946 7.77022 13 7.505 13C7.23978 13 6.98543 12.8946 6.79789 12.7071C6.61036 12.5196 6.505 12.2652 6.505 12C6.505 11.7348 6.61036 11.4804 6.79789 11.2929C6.98543 11.1054 7.23978 11 7.505 11ZM13.505 11C13.7702 11 14.0246 11.1054 14.2121 11.2929C14.3996 11.4804 14.505 11.7348 14.505 12C14.505 12.2652 14.3996 12.5196 14.2121 12.7071C14.0246 12.8946 13.7702 13 13.505 13C13.2398 13 12.9854 12.8946 12.7979 12.7071C12.6104 12.5196 12.505 12.2652 12.505 12C12.505 11.7348 12.6104 11.4804 12.7979 11.2929C12.9854 11.1054 13.2398 11 13.505 11ZM6 8C6.26522 8 6.51957 8.10536 6.70711 8.29289C6.89464 8.48043 7 8.73478 7 9C7 9.26522 6.89464 9.51957 6.70711 9.70711C6.51957 9.89464 6.26522 10 6 10C5.73478 10 5.48043 9.89464 5.29289 9.70711C5.10536 9.51957 5 9.26522 5 9C5 8.73478 5.10536 8.48043 5.29289 8.29289C5.48043 8.10536 5.73478 8 6 8ZM8.995 8C9.26022 8 9.51457 8.10536 9.70211 8.29289C9.88964 8.48043 9.995 8.73478 9.995 9C9.995 9.26522 9.88964 9.51957 9.70211 9.70711C9.51457 9.89464 9.26022 10 8.995 10C8.72978 10 8.47543 9.89464 8.28789 9.70711C8.10036 9.51957 7.995 9.26522 7.995 9C7.995 8.73478 8.10036 8.48043 8.28789 8.29289C8.47543 8.10536 8.72978 8 8.995 8ZM11.995 8C12.2602 8 12.5146 8.10536 12.7021 8.29289C12.8896 8.48043 12.995 8.73478 12.995 9C12.995 9.26522 12.8896 9.51957 12.7021 9.70711C12.5146 9.89464 12.2602 10 11.995 10C11.7298 10 11.4754 9.89464 11.2879 9.70711C11.1004 9.51957 10.995 9.26522 10.995 9C10.995 8.73478 11.1004 8.48043 11.2879 8.29289C11.4754 8.10536 11.7298 8 11.995 8ZM14.995 8C15.2602 8 15.5146 8.10536 15.7021 8.29289C15.8896 8.48043 15.995 8.73478 15.995 9C15.995 9.26522 15.8896 9.51957 15.7021 9.70711C15.5146 9.89464 15.2602 10 14.995 10C14.7298 10 14.4754 9.89464 14.2879 9.70711C14.1004 9.51957 13.995 9.26522 13.995 9C13.995 8.73478 14.1004 8.48043 14.2879 8.29289C14.4754 8.10536 14.7298 8 14.995 8ZM17.995 8C18.2602 8 18.5146 8.10536 18.7021 8.29289C18.8896 8.48043 18.995 8.73478 18.995 9C18.995 9.26522 18.8896 9.51957 18.7021 9.70711C18.5146 9.89464 18.2602 10 17.995 10C17.7298 10 17.4754 9.89464 17.2879 9.70711C17.1004 9.51957 16.995 9.26522 16.995 9C16.995 8.73478 17.1004 8.48043 17.2879 8.29289C17.4754 8.10536 17.7298 8 17.995 8Z" />
</svg>
);
};
export const CloseIcon = () => {
return (
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M6.88173 5.9864L11.8557 1.0257C11.954 0.911231 12.0054 0.76398 11.9996 0.613378C11.9937 0.462776 11.9311 0.319916 11.8243 0.213345C11.7174 0.106774 11.5742 0.0443419 11.4232 0.0385248C11.2722 0.0327077 11.1245 0.0839342 11.0097 0.181967L6.03573 5.14266L1.06173 0.175983C0.948744 0.063303 0.795507 0 0.635726 0C0.475945 0 0.322708 0.063303 0.209726 0.175983C0.0967439 0.288663 0.0332711 0.44149 0.0332711 0.600844C0.0332711 0.760197 0.0967439 0.913024 0.209726 1.0257L5.18973 5.9864L0.209726 10.9471C0.146917 11.0007 0.0959048 11.0668 0.0598909 11.141C0.0238769 11.2152 0.00363881 11.2961 0.000447115 11.3785C-0.00274458 11.4609 0.0111787 11.5431 0.0413434 11.6199C0.0715082 11.6967 0.117263 11.7664 0.175736 11.8247C0.234209 11.8831 0.304137 11.9287 0.381132 11.9588C0.458127 11.9889 0.540527 12.0027 0.623158 11.9996C0.70579 11.9964 0.786869 11.9762 0.861308 11.9403C0.935747 11.9044 1.00194 11.8535 1.05573 11.7908L6.03573 6.83014L11.0097 11.7908C11.1245 11.8889 11.2722 11.9401 11.4232 11.9343C11.5742 11.9285 11.7174 11.866 11.8243 11.7595C11.9311 11.6529 11.9937 11.51 11.9996 11.3594C12.0054 11.2088 11.954 11.0616 11.8557 10.9471L6.88173 5.9864Z" />
</svg>
);
};

View File

@@ -0,0 +1,73 @@
import { useState } from 'react';
import {
StyledIsland,
StyledIconWrapper,
StyledAnimateWrapper,
StyledTriggerWrapper,
} from './style';
import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './Icons';
import { Tooltip } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { useModal } from '@/store/globalModal';
import { MuiFade } from '@affine/component';
export type IslandItemNames = 'contact' | 'shortcuts';
export const HelpIsland = ({
showList = ['contact', 'shortcuts'],
}: {
showList?: IslandItemNames[];
}) => {
const [spread, setShowSpread] = useState(false);
const { triggerShortcutsModal, triggerContactModal } = useModal();
const { t } = useTranslation();
return (
<StyledIsland
spread={spread}
onClick={() => {
setShowSpread(!spread);
}}
>
<StyledAnimateWrapper spread={spread}>
{showList.includes('contact') && (
<Tooltip content={t('Contact Us')} placement="left-end">
<StyledIconWrapper
data-testid="right-bottom-contact-us-icon"
onClick={() => {
setShowSpread(false);
triggerContactModal();
}}
>
<ContactIcon />
</StyledIconWrapper>
</Tooltip>
)}
{showList.includes('shortcuts') && (
<Tooltip content={t('Keyboard Shortcuts')} placement="left-end">
<StyledIconWrapper
data-testid="shortcuts-icon"
onClick={() => {
setShowSpread(false);
triggerShortcutsModal();
}}
>
<KeyboardIcon />
</StyledIconWrapper>
</Tooltip>
)}
</StyledAnimateWrapper>
<MuiFade in={!spread} data-testid="faq-icon">
<StyledTriggerWrapper>
<HelpIcon />
</StyledTriggerWrapper>
</MuiFade>
<MuiFade in={spread}>
<StyledTriggerWrapper>
<CloseIcon />
</StyledTriggerWrapper>
</MuiFade>
</StyledIsland>
);
};
export default HelpIsland;

View File

@@ -0,0 +1,77 @@
import { displayFlex, positionAbsolute, styled } from '@affine/component';
export const StyledIsland = styled('div')<{
spread: boolean;
}>(({ theme, spread }) => {
return {
transition: 'box-shadow 0.2s',
width: '44px',
position: 'relative',
boxShadow: spread
? '4px 4px 7px rgba(58, 76, 92, 0.04), -4px -4px 13px rgba(58, 76, 92, 0.02), 6px 6px 36px rgba(58, 76, 92, 0.06)'
: 'unset',
padding: '0 4px 44px',
borderRadius: '10px',
backgroundColor: theme.colors.pageBackground,
':hover': {
boxShadow:
'4px 4px 7px rgba(58, 76, 92, 0.04), -4px -4px 13px rgba(58, 76, 92, 0.02), 6px 6px 36px rgba(58, 76, 92, 0.06)',
},
'::after': {
content: '""',
width: '36px',
height: '1px',
background: spread ? theme.colors.borderColor : 'transparent',
...positionAbsolute({
left: 0,
right: 0,
bottom: '44px',
}),
margin: 'auto',
transition: 'background 0.15s',
},
};
});
export const StyledIconWrapper = styled('div')(({ theme }) => {
return {
color: theme.colors.iconColor,
...displayFlex('center', 'center'),
cursor: 'pointer',
backgroundColor: theme.colors.pageBackground,
borderRadius: '5px',
width: '36px',
height: '36px',
margin: '4px auto 4px',
transition: 'background-color 0.2s',
position: 'relative',
':hover': {
color: theme.colors.primaryColor,
backgroundColor: theme.colors.hoverBackground,
},
};
});
export const StyledAnimateWrapper = styled('div', {
shouldForwardProp: prop => prop !== 'spread',
})<{ spread: boolean }>(({ spread }) => ({
height: spread ? '88px' : '0',
transition: 'height 0.2s cubic-bezier(0, 0, 0.55, 1.6)',
overflow: 'hidden',
}));
export const StyledTriggerWrapper = styled('div')(({ theme }) => {
return {
width: '36px',
height: '36px',
cursor: 'pointer',
backgroundColor: theme.colors.pageBackground,
color: theme.colors.iconColor,
borderRadius: '5px',
...displayFlex('center', 'center'),
...positionAbsolute({ left: '4px', bottom: '4px' }),
':hover': {
color: theme.colors.primaryColor,
backgroundColor: theme.colors.hoverBackground,
},
};
});

View File

@@ -0,0 +1,136 @@
import { Modal, ModalWrapper, ModalCloseButton } from '@affine/component';
import { StyledButtonWrapper, StyledTitle } from './styles';
import { Button } from '@affine/component';
import { Content, FlexWrapper } from '@affine/component';
import Loading from '@/components/loading';
import { usePageHelper } from '@/hooks/use-page-helper';
import { useAppState } from '@/providers/app-state-provider';
import { useEffect, useState } from 'react';
import { useTranslation } from '@affine/i18n';
// import { Tooltip } from '@affine/component';
type ImportModalProps = {
open: boolean;
onClose: () => void;
};
type Template = {
name: string;
source: string;
};
export const ImportModal = ({ open, onClose }: ImportModalProps) => {
const [status, setStatus] = useState<'unImported' | 'importing'>('importing');
const { openPage, createPage } = usePageHelper();
const { currentWorkspace } = useAppState();
const { t } = useTranslation();
const _applyTemplate = function (pageId: string, template: Template) {
const page = currentWorkspace?.blocksuiteWorkspace?.getPage(pageId);
const title = template.name;
if (page) {
currentWorkspace?.blocksuiteWorkspace?.setPageMeta(page.id, { title });
if (page.root === null) {
setTimeout(() => {
try {
const editor = document.querySelector('editor-container');
if (editor) {
page.addBlock({ flavour: 'affine:surface' }, null);
const frameId = page.addBlock(
{ flavour: 'affine:frame' },
pageId
);
// TODO blocksuite should offer a method to import markdown from store
editor.clipboard.importMarkdown(template.source, `${frameId}`);
page.resetHistory();
editor.requestUpdate();
}
} catch (e) {
console.error(e);
}
}, 300);
}
}
};
const _handleAppleTemplate = async function (template: Template) {
const pageId = await createPage();
if (pageId) {
openPage(pageId);
_applyTemplate(pageId, template);
}
};
const _handleAppleTemplateFromFilePicker = async () => {
if (!window.showOpenFilePicker) {
return;
}
const arrFileHandle = await window.showOpenFilePicker({
types: [
{
accept: {
'text/markdown': ['.md'],
'text/html': ['.html', '.htm'],
'text/plain': ['.text'],
},
},
],
multiple: false,
});
for (const fileHandle of arrFileHandle) {
const file = await fileHandle.getFile();
const text = await file.text();
_handleAppleTemplate({
name: file.name,
source: text,
});
}
onClose && onClose();
};
useEffect(() => {
if (status === 'importing') {
setTimeout(() => {
setStatus('unImported');
}, 1500);
}
}, [status]);
return (
<Modal open={open} onClose={onClose}>
<ModalWrapper width={460} minHeight={240}>
<ModalCloseButton onClick={onClose} />
<StyledTitle>{t('Import')}</StyledTitle>
{status === 'unImported' && (
<StyledButtonWrapper>
<Button
onClick={() => {
_handleAppleTemplateFromFilePicker();
}}
>
Markdown
</Button>
<Button
onClick={() => {
_handleAppleTemplateFromFilePicker();
}}
>
HTML
</Button>
</StyledButtonWrapper>
)}
{status === 'importing' && (
<FlexWrapper
wrap={true}
justifyContent="center"
style={{ marginTop: 22, paddingBottom: '32px' }}
>
<Loading size={25}></Loading>
<Content align="center" weight="500">
OOOOPS! Sorry forgot to remind you that we are working on the
import function
</Content>
</FlexWrapper>
)}
</ModalWrapper>
</Modal>
);
};
export default ImportModal;

View File

@@ -0,0 +1,25 @@
import { styled } from '@affine/component';
export const StyledTitle = styled.div(({ theme }) => {
return {
fontSize: theme.font.h6,
fontWeight: 600,
textAlign: 'center',
marginTop: '45px',
color: theme.colors.popoverColor,
};
});
export const StyledButtonWrapper = styled.div(() => {
return {
width: '280px',
margin: '24px auto 0',
button: {
display: 'block',
width: '100%',
':not(:last-child)': {
marginBottom: '16px',
},
},
};
});

View File

@@ -0,0 +1,20 @@
import {
StyledLoadingWrapper,
StyledLoading,
StyledLoadingItem,
} from './styled';
export const Loading = ({ size = 40 }: { size?: number }) => {
return (
<StyledLoadingWrapper size={size}>
<StyledLoading>
<StyledLoadingItem size={size} />
<StyledLoadingItem size={size} />
<StyledLoadingItem size={size} />
<StyledLoadingItem size={size} />
</StyledLoading>
</StyledLoadingWrapper>
);
};
export default Loading;

View File

@@ -0,0 +1,33 @@
import { styled } from '@affine/component';
import Loading from './Loading';
import { useTranslation } from '@affine/i18n';
// Used for the full page loading
const StyledLoadingContainer = styled('div')(() => {
return {
height: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: '#6880FF',
h1: {
fontSize: '2em',
marginTop: '15px',
fontWeight: '600',
},
};
});
export const PageLoading = ({ text }: { text?: string }) => {
const { t } = useTranslation();
return (
<StyledLoadingContainer>
<div className="wrapper">
<Loading />
<h1>{text ? text : t('Loading')}</h1>
</div>
</StyledLoadingContainer>
);
};
export default PageLoading;

View File

@@ -0,0 +1,3 @@
import Loading from './Loading';
export * from './PageLoading';
export default Loading;

View File

@@ -0,0 +1,94 @@
import { styled } from '@affine/component';
// Inspired by https://codepen.io/graphilla/pen/rNvBMYY
export const StyledLoadingWrapper = styled('div', {
shouldForwardProp: prop => {
return !['size'].includes(prop);
},
})<{ size?: number }>(({ size = 40 }) => {
return {
width: size * 4,
height: size * 4,
position: 'relative',
};
});
export const StyledLoading = styled.div`
position: absolute;
left: 25%;
top: 50%;
transform: rotateX(55deg) rotateZ(-45deg);
@keyframes slide {
0% {
transform: translate(var(--sx), var(--sy));
}
65% {
transform: translate(var(--ex), var(--sy));
}
95%,
100% {
transform: translate(var(--ex), var(--ey));
}
}
`;
export const StyledLoadingItem = styled.div<{ size: number }>(
({ size = 40 }) => {
return `
position: absolute;
width: ${size}px;
height: ${size}px;
background: #9dacf9;
animation: slide 0.9s cubic-bezier(0.65, 0.53, 0.59, 0.93) infinite;
&::before,
&::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
}
&::before {
background: #5260b9;
transform: skew(0deg, -45deg);
right: 100%;
top: 50%;
}
&::after {
background: #6880ff;
transform: skew(-45deg, 0deg);
top: 100%;
right: 50%;
}
&:nth-of-type(1) {
--sx: 50%;
--sy: -50%;
--ex: 150%;
--ey: 50%;
}
&:nth-of-type(2) {
--sx: -50%;
--sy: -50%;
--ex: 50%;
--ey: -50%;
}
&:nth-of-type(3) {
--sx: 150%;
--sy: 50%;
--ex: 50%;
--ey: 50%;
}
&:nth-of-type(4) {
--sx: 50%;
--sy: 50%;
--ex: -50%;
--ey: -50%;
}
`;
}
);

View File

@@ -0,0 +1,28 @@
export const GoogleIcon = () => {
return (
<svg
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22.3055 10.0415H21.5V10H12.5V14H18.1515C17.327 16.3285 15.1115 18 12.5 18C9.1865 18 6.5 15.3135 6.5 12C6.5 8.6865 9.1865 6 12.5 6C14.0295 6 15.421 6.577 16.4805 7.5195L19.309 4.691C17.523 3.0265 15.134 2 12.5 2C6.9775 2 2.5 6.4775 2.5 12C2.5 17.5225 6.9775 22 12.5 22C18.0225 22 22.5 17.5225 22.5 12C22.5 11.3295 22.431 10.675 22.3055 10.0415Z"
fill="#FFC107"
/>
<path
d="M3.65234 7.3455L6.93784 9.755C7.82684 7.554 9.97984 6 12.4993 6C14.0288 6 15.4203 6.577 16.4798 7.5195L19.3083 4.691C17.5223 3.0265 15.1333 2 12.4993 2C8.65834 2 5.32734 4.1685 3.65234 7.3455Z"
fill="#FF3D00"
/>
<path
d="M12.5002 22.0003C15.0832 22.0003 17.4302 21.0118 19.2047 19.4043L16.1097 16.7853C15.0719 17.5745 13.8039 18.0014 12.5002 18.0003C9.89916 18.0003 7.69066 16.3418 6.85866 14.0273L3.59766 16.5398C5.25266 19.7783 8.61366 22.0003 12.5002 22.0003Z"
fill="#4CAF50"
/>
<path
d="M22.3055 10.0415H21.5V10H12.5V14H18.1515C17.7571 15.1082 17.0467 16.0766 16.108 16.7855L16.1095 16.7845L19.2045 19.4035C18.9855 19.6025 22.5 17 22.5 12C22.5 11.3295 22.431 10.675 22.3055 10.0415Z"
fill="#1976D2"
/>
</svg>
);
};

View File

@@ -0,0 +1,75 @@
import { positionAbsolute, styled } from '@affine/component';
import { Modal, ModalWrapper, ModalCloseButton } from '@affine/component';
import { Button } from '@affine/component';
import { useAppState } from '@/providers/app-state-provider';
import { useTranslation } from '@affine/i18n';
import { GoogleIcon } from './GoogleIcon';
interface LoginModalProps {
open: boolean;
onClose: () => void;
}
export const LoginModal = ({ open, onClose }: LoginModalProps) => {
const { login } = useAppState();
const { t } = useTranslation();
return (
<Modal open={open} onClose={onClose} data-testid="login-modal">
<ModalWrapper width={560} height={292} style={{ paddingTop: '44px' }}>
<ModalCloseButton
onClick={() => {
onClose();
}}
/>
<Content>
<ContentTitle>{t('Sign in')}</ContentTitle>
<SignDes>{t('Set up an AFFiNE account to sync data')}</SignDes>
<StyledLoginButton
shape="round"
onClick={async () => {
await login();
onClose();
}}
>
<GoogleIcon />
{t('Continue with Google')}
</StyledLoginButton>
</Content>
</ModalWrapper>
</Modal>
);
};
const StyledLoginButton = styled(Button)(() => {
return {
width: '284px',
marginTop: '30px',
position: 'relative',
svg: {
...positionAbsolute({ left: '18px', top: '0', bottom: '0' }),
margin: 'auto',
},
};
});
const Content = styled('div')({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '16px',
});
const ContentTitle = styled('h1')({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'center',
paddingBottom: '16px',
});
const SignDes = styled('div')(({ theme }) => {
return {
fontWeight: 400,
color: theme.colors.textColor,
fontSize: '16px',
};
});

View File

@@ -0,0 +1,48 @@
export const Check = () => {
return (
<span>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_9266_16831)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.83301 3.33331C4.4523 3.33331 3.33301 4.4526 3.33301 5.83331V14.1666C3.33301 15.5474 4.4523 16.6666 5.83301 16.6666H14.1663C15.5471 16.6666 16.6663 15.5474 16.6663 14.1666V5.83331C16.6663 4.4526 15.5471 3.33331 14.1663 3.33331H5.83301ZM14.6385 7.97059C14.8984 7.70982 14.8977 7.28771 14.6369 7.02778C14.3762 6.76785 13.9541 6.76852 13.6941 7.02929L8.62861 12.1111L6.30522 9.77929C6.04534 9.51847 5.62323 9.51771 5.36241 9.77759C5.10159 10.0375 5.10083 10.4596 5.36071 10.7204L8.03822 13.4076C8.36386 13.7344 8.89304 13.7344 9.21874 13.4077L14.6385 7.97059Z"
fill="#888A9E"
/>
</g>
<defs>
<clipPath id="clip0_9266_16831">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>
</span>
);
};
export const UnCheck = () => {
return (
<span>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.1673 4.66665H5.83398C5.18965 4.66665 4.66732 5.18898 4.66732 5.83331V14.1666C4.66732 14.811 5.18965 15.3333 5.83398 15.3333H14.1673C14.8116 15.3333 15.334 14.811 15.334 14.1666V5.83331C15.334 5.18898 14.8116 4.66665 14.1673 4.66665ZM5.83398 3.33331C4.45327 3.33331 3.33398 4.4526 3.33398 5.83331V14.1666C3.33398 15.5474 4.45327 16.6666 5.83398 16.6666H14.1673C15.548 16.6666 16.6673 15.5474 16.6673 14.1666V5.83331C16.6673 4.4526 15.548 3.33331 14.1673 3.33331H5.83398Z"
fill="#888A9E"
/>
</svg>
</span>
);
};

View File

@@ -0,0 +1,147 @@
import { styled } from '@affine/component';
import { Modal, ModalWrapper, ModalCloseButton } from '@affine/component';
import { Button } from '@affine/component';
import { Check, UnCheck } from './icon';
import { useState } from 'react';
import { useTranslation } from '@affine/i18n';
import { useAppState } from '@/providers/app-state-provider';
interface LoginModalProps {
open: boolean;
onClose: (wait: boolean) => void;
}
export const LogoutModal = ({ open, onClose }: LoginModalProps) => {
const [localCache, setLocalCache] = useState(true);
const { blobDataSynced } = useAppState();
const { t } = useTranslation();
return (
<Modal open={open} onClose={onClose} data-testid="logout-modal">
<ModalWrapper width={560} height={292}>
<Header>
<ModalCloseButton
onClick={() => {
onClose(true);
}}
/>
</Header>
<Content>
<ContentTitle>{t('Sign out')}?</ContentTitle>
<SignDes>
{blobDataSynced
? t('Set up an AFFiNE account to sync data')
: 'All data has been stored in the cloud'}
</SignDes>
<StyleTips>
{localCache ? (
<StyleCheck
onClick={() => {
setLocalCache(false);
}}
>
<Check></Check>
</StyleCheck>
) : (
<StyleCheck
onClick={() => {
setLocalCache(true);
}}
>
<UnCheck></UnCheck>
</StyleCheck>
)}
{t('Retain local cached data')}
</StyleTips>
{blobDataSynced ? (
<div>
<Button
type="danger"
shape="round"
style={{ marginRight: '16px' }}
onClick={async () => {
onClose(false);
}}
>
{t('Force Sign Out')}
</Button>
<Button
shape="round"
onClick={() => {
onClose(true);
}}
>
{t('Wait for Sync')}
</Button>
</div>
) : (
<div>
<Button
type="primary"
style={{ marginRight: '16px' }}
shape="round"
onClick={() => {
onClose(true);
}}
>
{t('Cancel')}
</Button>
<Button
shape="round"
onClick={() => {
onClose(false);
}}
>
{t('Sign out')}
</Button>
</div>
)}
</Content>
</ModalWrapper>
</Modal>
);
};
const Header = styled('div')({
position: 'relative',
height: '44px',
});
const Content = styled('div')({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '16px',
});
const ContentTitle = styled('h1')({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'center',
paddingBottom: '16px',
});
const SignDes = styled('div')(({ theme }) => {
return {
fontWeight: 400,
color: theme.colors.textColor,
fontSize: '16px',
};
});
const StyleCheck = styled('span')(() => {
return {
display: 'inline-block',
cursor: 'pointer',
svg: {
verticalAlign: 'sub',
marginRight: '8px',
},
};
});
const StyleTips = styled('span')(() => {
return {
userSelect: 'none',
};
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

View File

@@ -0,0 +1,47 @@
import React, { useState } from 'react';
import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component';
import getIsMobile from '@/utils/get-is-mobile';
import { StyledButton, StyledContent, StyledTitle } from './styles';
import bg from './bg.png';
import { useTranslation } from '@affine/i18n';
export const MobileModal = () => {
const [showModal, setShowModal] = useState(getIsMobile());
const { t } = useTranslation();
return (
<Modal
open={showModal}
onClose={() => {
setShowModal(false);
}}
>
<ModalWrapper
width={348}
height={388}
style={{ backgroundImage: `url(${bg.src})` }}
>
<ModalCloseButton
size={[30, 30]}
iconSize={[20, 20]}
onClick={() => {
setShowModal(false);
}}
/>
<StyledTitle>{t('Ooops!')}</StyledTitle>
<StyledContent>
<p>{t('mobile device')}</p>
<p>{t('mobile device description')}</p>
</StyledContent>
<StyledButton
onClick={() => {
setShowModal(false);
}}
>
{t('Got it')}
</StyledButton>
</ModalWrapper>
</Modal>
);
};
export default MobileModal;

View File

@@ -0,0 +1,38 @@
import { displayFlex, styled } from '@affine/component';
export const StyledTitle = styled.div(() => {
return {
...displayFlex('center', 'center'),
fontSize: '20px',
fontWeight: 500,
marginTop: '60px',
lineHeight: 1,
};
});
export const StyledContent = styled.div(() => {
return {
padding: '0 40px',
marginTop: '32px',
fontSize: '18px',
lineHeight: '25px',
'p:not(last-of-type)': {
marginBottom: '10px',
},
};
});
export const StyledButton = styled.div(({ theme }) => {
return {
width: '146px',
height: '42px',
background: theme.colors.primaryColor,
color: '#FFFFFF',
fontSize: '18px',
fontWeight: 500,
borderRadius: '21px',
margin: '52px auto 0',
cursor: 'pointer',
...displayFlex('center', 'center'),
};
});

View File

@@ -0,0 +1,27 @@
import localizedFormat from 'dayjs/plugin/localizedFormat';
import dayjs from 'dayjs';
import { PageMeta } from '@/providers/app-state-provider';
import { TableCell } from '@affine/component';
import React from 'react';
dayjs.extend(localizedFormat);
export const DateCell = ({
pageMeta,
dateKey,
backupKey = '',
}: {
pageMeta: PageMeta;
dateKey: keyof PageMeta;
backupKey?: keyof PageMeta;
}) => {
// dayjs().format('L LT');
const value = pageMeta[dateKey] ?? pageMeta[backupKey];
return (
<TableCell ellipsis={true}>
{value ? dayjs(value as string).format('YYYY-MM-DD HH:mm') : '--'}
</TableCell>
);
};
export default DateCell;

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { Empty } from '@affine/component';
import { useTranslation } from '@affine/i18n';
export const PageListEmpty = (props: { listType?: string }) => {
const { listType } = props;
const { t } = useTranslation();
return (
<div style={{ textAlign: 'center' }}>
<Empty
width={800}
height={300}
sx={{ marginTop: '100px', marginBottom: '30px' }}
/>
{listType === 'all' && <p>{t('emptyAllPages')}</p>}
{listType === 'favorite' && <p>{t('emptyFavorite')}</p>}
{listType === 'trash' && <p>{t('emptyTrash')}</p>}
</div>
);
};
export default PageListEmpty;

View File

@@ -0,0 +1,113 @@
import { useConfirm } from '@/providers/ConfirmProvider';
import { PageMeta } from '@/providers/app-state-provider';
import { Menu, MenuItem } from '@affine/component';
import { FlexWrapper } from '@affine/component';
import { IconButton } from '@affine/component';
import {
MoreVerticalIcon,
RestoreIcon,
DeleteIcon,
FavouritesIcon,
FavouritedIcon,
OpenInNewIcon,
TrashIcon,
} from '@blocksuite/icons';
import { toast } from '@affine/component';
import { usePageHelper } from '@/hooks/use-page-helper';
import { useTranslation } from '@affine/i18n';
export const OperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
const { id, favorite } = pageMeta;
const { openPage } = usePageHelper();
const { toggleFavoritePage, toggleDeletePage } = usePageHelper();
const { confirm } = useConfirm();
const { t } = useTranslation();
const OperationMenu = (
<>
<MenuItem
onClick={() => {
toggleFavoritePage(id);
toast(
favorite ? t('Removed from Favorites') : t('Added to Favorites')
);
}}
icon={favorite ? <FavouritedIcon /> : <FavouritesIcon />}
>
{favorite ? t('Remove from favorites') : t('Add to favorites')}
</MenuItem>
<MenuItem
onClick={() => {
openPage(id, {}, true);
}}
icon={<OpenInNewIcon />}
>
{t('Open in new tab')}
</MenuItem>
<MenuItem
onClick={() => {
confirm({
title: t('Delete page?'),
content: t('will be moved to Trash', {
title: pageMeta.title || 'Untitled',
}),
confirmText: t('Delete'),
confirmType: 'danger',
}).then(confirm => {
confirm && toggleDeletePage(id);
toast(t('Moved to Trash'));
});
}}
icon={<TrashIcon />}
>
{t('Delete')}
</MenuItem>
</>
);
return (
<FlexWrapper alignItems="center" justifyContent="center">
<Menu content={OperationMenu} placement="bottom-end" disablePortal={true}>
<IconButton darker={true}>
<MoreVerticalIcon />
</IconButton>
</Menu>
</FlexWrapper>
);
};
export const TrashOperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
const { id } = pageMeta;
const { openPage, getPageMeta } = usePageHelper();
const { toggleDeletePage, permanentlyDeletePage } = usePageHelper();
const { confirm } = useConfirm();
const { t } = useTranslation();
return (
<FlexWrapper>
<IconButton
darker={true}
style={{ marginRight: '12px' }}
onClick={() => {
toggleDeletePage(id);
toast(t('restored', { title: getPageMeta(id)?.title || 'Untitled' }));
openPage(id);
}}
>
<RestoreIcon />
</IconButton>
<IconButton
darker={true}
onClick={() => {
confirm({
title: t('Delete permanently?'),
content: t("Once deleted, you can't undo this action."),
confirmText: t('Delete'),
confirmType: 'danger',
}).then(confirm => {
confirm && permanentlyDeletePage(id);
toast(t('Permanently deleted'));
});
}}
>
<DeleteIcon />
</IconButton>
</FlexWrapper>
);
};

View File

@@ -0,0 +1,168 @@
import { PageMeta } from '@/providers/app-state-provider';
import {
FavouritedIcon,
FavouritesIcon,
PaperIcon,
EdgelessIcon,
} from '@blocksuite/icons';
import {
StyledTableContainer,
StyledTableRow,
StyledTitleLink,
StyledTitleWrapper,
} from './styles';
import {
Table,
TableBody,
TableCell,
TableHead,
TableRow,
} from '@affine/component';
import { OperationCell, TrashOperationCell } from './OperationCell';
import Empty from './Empty';
import { Content } from '@affine/component';
import React from 'react';
import DateCell from '@/components/page-list/DateCell';
import { IconButton } from '@affine/component';
import { Tooltip } from '@affine/component';
import { useRouter } from 'next/router';
import { useAppState } from '@/providers/app-state-provider';
import { toast } from '@affine/component';
import { usePageHelper } from '@/hooks/use-page-helper';
import { useTheme } from '@/providers/ThemeProvider';
import { useTranslation } from '@affine/i18n';
const FavoriteTag = ({
pageMeta: { favorite, id },
}: {
pageMeta: PageMeta;
}) => {
const { toggleFavoritePage } = usePageHelper();
const { theme } = useTheme();
const { t } = useTranslation();
return (
<Tooltip
content={favorite ? t('Favorited') : t('Favorite')}
placement="top-start"
>
<IconButton
darker={true}
iconSize={[20, 20]}
onClick={e => {
e.stopPropagation();
toggleFavoritePage(id);
toast(
favorite ? t('Removed from Favorites') : t('Added to Favorites')
);
}}
style={{
color: favorite ? theme.colors.primaryColor : theme.colors.iconColor,
}}
className={favorite ? '' : 'favorite-button'}
>
{favorite ? (
<FavouritedIcon data-testid="favorited-icon" />
) : (
<FavouritesIcon />
)}
</IconButton>
</Tooltip>
);
};
export const PageList = ({
pageList,
showFavoriteTag = false,
isTrash = false,
isPublic = false,
listType,
}: {
pageList: PageMeta[];
showFavoriteTag?: boolean;
isTrash?: boolean;
isPublic?: boolean;
listType?: 'all' | 'trash' | 'favorite';
}) => {
const router = useRouter();
const { currentWorkspace } = useAppState();
const { t } = useTranslation();
if (pageList.length === 0) {
return <Empty listType={listType} />;
}
return (
<StyledTableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell proportion={0.5}>{t('Title')}</TableCell>
<TableCell proportion={0.2}>{t('Created')}</TableCell>
<TableCell proportion={0.2}>
{isTrash ? t('Moved to Trash') : t('Updated')}
</TableCell>
<TableCell proportion={0.1}></TableCell>
</TableRow>
</TableHead>
<TableBody>
{pageList.map((pageMeta, index) => {
return (
<StyledTableRow
data-testid="page-list-item"
key={`${pageMeta.id}-${index}`}
onClick={() => {
if (isPublic) {
router.push(
`/public-workspace/${router.query.workspaceId}/${pageMeta.id}`
);
} else {
router.push(
`/workspace/${currentWorkspace?.id}/${pageMeta.id}`
);
}
}}
>
<TableCell>
<StyledTitleWrapper>
<StyledTitleLink>
{pageMeta.mode === 'edgeless' ? (
<EdgelessIcon />
) : (
<PaperIcon />
)}
<Content ellipsis={true} color="inherit">
{pageMeta.title || t('Untitled')}
</Content>
</StyledTitleLink>
{showFavoriteTag && <FavoriteTag pageMeta={pageMeta} />}
</StyledTitleWrapper>
</TableCell>
<DateCell pageMeta={pageMeta} dateKey="createDate" />
<DateCell
pageMeta={pageMeta}
dateKey={isTrash ? 'trashDate' : 'updatedDate'}
backupKey={isTrash ? 'trashDate' : 'createDate'}
/>
{!isPublic ? (
<TableCell
style={{ padding: 0 }}
data-testid={`more-actions-${pageMeta.id}`}
onClick={e => {
e.stopPropagation();
}}
>
{isTrash ? (
<TrashOperationCell pageMeta={pageMeta} />
) : (
<OperationCell pageMeta={pageMeta} />
)}
</TableCell>
) : null}
</StyledTableRow>
);
})}
</TableBody>
</Table>
</StyledTableContainer>
);
};
export default PageList;

View File

@@ -0,0 +1,51 @@
import { displayFlex, styled } from '@affine/component';
import { TableRow } from '@affine/component';
export const StyledTableContainer = styled.div(() => {
return {
height: 'calc(100vh - 60px)',
padding: '78px 72px',
overflowY: 'auto',
};
});
export const StyledTitleWrapper = styled.div(({ theme }) => {
return {
...displayFlex('flex-start', 'center'),
a: {
color: 'inherit',
},
'a:visited': {
color: 'unset',
},
'a:hover': {
color: theme.colors.primaryColor,
},
};
});
export const StyledTitleLink = styled.div(({ theme }) => {
return {
maxWidth: '80%',
marginRight: '18px',
...displayFlex('flex-start', 'center'),
color: theme.colors.textColor,
'>svg': {
fontSize: '24px',
marginRight: '12px',
color: theme.colors.iconColor,
},
};
});
export const StyledTableRow = styled(TableRow)(() => {
return {
cursor: 'pointer',
'.favorite-button': {
display: 'none',
},
'&:hover': {
'.favorite-button': {
display: 'flex',
},
},
};
});

View File

@@ -0,0 +1,18 @@
import { cloneElement, FC, PropsWithChildren, ReactNode } from 'react';
export const ProviderComposer: FC<
PropsWithChildren<{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
contexts: any;
}>
> = ({ contexts, children }) =>
contexts.reduceRight(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(kids: ReactNode, parent: any) =>
cloneElement(parent, {
children: kids,
}),
children
);
export default ProviderComposer;

View File

@@ -0,0 +1,33 @@
import React from 'react';
import { AddIcon } from '@blocksuite/icons';
import { StyledModalFooterContent } from './style';
import { Command } from 'cmdk';
import { usePageHelper } from '@/hooks/use-page-helper';
import { useTranslation } from '@affine/i18n';
export const Footer = (props: { query: string; onClose: () => void }) => {
const { openPage, createPage } = usePageHelper();
const { t } = useTranslation();
const { query, onClose } = props;
return (
<Command.Item
data-testid="quickSearch-addNewPage"
onSelect={async () => {
onClose();
const pageId = await createPage({ title: query });
if (pageId) {
openPage(pageId);
}
}}
>
<StyledModalFooterContent>
<AddIcon />
{query ? (
<span>{t('New Keyword Page', { query: query })}</span>
) : (
<span>{t('New Page')}</span>
)}
</StyledModalFooterContent>
</Command.Item>
);
};

View File

@@ -0,0 +1,99 @@
import React, {
Dispatch,
SetStateAction,
useEffect,
useRef,
useState,
} from 'react';
import { SearchIcon } from '@blocksuite/icons';
import { StyledInputContent, StyledLabel } from './style';
import { Command } from 'cmdk';
import { useTranslation } from '@affine/i18n';
export const Input = (props: {
open: boolean;
query: string;
setQuery: Dispatch<SetStateAction<string>>;
setLoading: Dispatch<SetStateAction<boolean>>;
isPublic: boolean;
publishWorkspaceName: string | undefined;
}) => {
const { open, query, setQuery, setLoading, isPublic, publishWorkspaceName } =
props;
const [isComposition, setIsComposition] = useState(false);
const [inputValue, setInputValue] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
const { t } = useTranslation();
useEffect(() => {
if (open) {
const inputElement = inputRef.current;
return inputElement?.focus();
}
}, [open]);
useEffect(() => {
const inputElement = inputRef.current;
if (!open) {
return;
}
const handleFocus = () => {
inputElement?.focus();
};
inputElement?.addEventListener('blur', handleFocus, true);
return () => inputElement?.removeEventListener('blur', handleFocus, true);
}, [inputRef, open]);
useEffect(() => {
setInputValue(query);
}, [query]);
return (
<StyledInputContent>
<StyledLabel htmlFor=":r5:">
<SearchIcon />
</StyledLabel>
<Command.Input
ref={inputRef}
value={inputValue}
onCompositionStart={() => {
setIsComposition(true);
}}
onCompositionEnd={e => {
setQuery(e.data);
setIsComposition(false);
if (!query) {
setLoading(true);
}
}}
onValueChange={str => {
setInputValue(str);
if (!isComposition) {
setQuery(str);
if (!query) {
setLoading(true);
}
}
}}
onKeyDown={(e: React.KeyboardEvent) => {
if (e.key === 'a' && e.metaKey) {
e.stopPropagation();
inputRef.current?.select();
return;
}
if (isComposition) {
if (
e.key === 'ArrowDown' ||
e.key === 'ArrowUp' ||
e.key === 'Enter'
) {
e.stopPropagation();
}
}
}}
placeholder={
isPublic
? t('Quick search placeholder2', {
workspace: publishWorkspaceName,
})
: t('Quick search placeholder')
}
/>
</StyledInputContent>
);
};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,95 @@
import { Command } from 'cmdk';
import { StyledListItem, StyledNotFound } from './style';
import { PaperIcon, EdgelessIcon } from '@blocksuite/icons';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useAppState, PageMeta } from '@/providers/app-state-provider';
import { useRouter } from 'next/router';
import { NoResultSVG } from './NoResultSVG';
import { useTranslation } from '@affine/i18n';
import usePageHelper from '@/hooks/use-page-helper';
import { Workspace } from '@blocksuite/store';
export const PublishedResults = (props: {
query: string;
loading: boolean;
setLoading: Dispatch<SetStateAction<boolean>>;
setPublishWorkspaceName: Dispatch<SetStateAction<string>>;
onClose: () => void;
}) => {
const [workspace, setWorkspace] = useState<Workspace>();
const { query, loading, setLoading, onClose, setPublishWorkspaceName } =
props;
const { search } = usePageHelper();
const [results, setResults] = useState(new Map<string, string | undefined>());
const { dataCenter } = useAppState();
const router = useRouter();
const [pageList, setPageList] = useState<PageMeta[]>([]);
useEffect(() => {
dataCenter
.loadPublicWorkspace(router.query.workspaceId as string)
.then(data => {
setPageList(data.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]);
if (data.blocksuiteWorkspace) {
setWorkspace(data.blocksuiteWorkspace);
setPublishWorkspaceName(data.blocksuiteWorkspace.meta.name);
}
})
.catch(() => {
router.push('/404');
});
}, [router, dataCenter, setPublishWorkspaceName]);
const { t } = useTranslation();
useEffect(() => {
setResults(search(query, workspace));
setLoading(false);
//Save the Map<BlockId, PageId> obtained from the search as state
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query, setResults, setLoading]);
const pageIds = [...results.values()];
const resultsPageMeta = pageList.filter(
page => pageIds.indexOf(page.id) > -1 && !page.trash
);
return loading ? null : (
<>
{query ? (
resultsPageMeta.length ? (
<Command.Group
heading={t('Find results', { number: resultsPageMeta.length })}
>
{resultsPageMeta.map(result => {
return (
<Command.Item
key={result.id}
onSelect={() => {
router.push(
`/public-workspace/${router.query.workspaceId}/${result.id}`
);
onClose();
}}
value={result.id}
>
<StyledListItem>
{result.mode === 'edgeless' ? (
<EdgelessIcon />
) : (
<PaperIcon />
)}
<span>{result.title}</span>
</StyledListItem>
</Command.Item>
);
})}
</Command.Group>
) : (
<StyledNotFound>
<span>{t('Find 0 result')}</span>
<NoResultSVG />
</StyledNotFound>
)
) : (
<></>
)}
</>
);
};

View File

@@ -0,0 +1,100 @@
import { Command } from 'cmdk';
import { StyledListItem, StyledNotFound } from './style';
import { PaperIcon, EdgelessIcon } from '@blocksuite/icons';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useAppState } from '@/providers/app-state-provider';
import { useRouter } from 'next/router';
import { useSwitchToConfig } from './config';
import { NoResultSVG } from './NoResultSVG';
import { useTranslation } from '@affine/i18n';
import usePageHelper from '@/hooks/use-page-helper';
export const Results = (props: {
query: string;
loading: boolean;
onClose: () => void;
setLoading: Dispatch<SetStateAction<boolean>>;
setShowCreatePage: Dispatch<SetStateAction<boolean>>;
}) => {
const { query, loading, setLoading, setShowCreatePage, onClose } = props;
const { openPage } = usePageHelper();
const router = useRouter();
const { currentWorkspace, pageList } = useAppState();
const { search } = usePageHelper();
const List = useSwitchToConfig(currentWorkspace?.id);
const [results, setResults] = useState(new Map<string, string | undefined>());
const { t } = useTranslation();
useEffect(() => {
setResults(search(query));
setLoading(false);
//Save the Map<BlockId, PageId> obtained from the search as state
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query, setResults, setLoading]);
const pageIds = [...results.values()];
const resultsPageMeta = pageList.filter(
page => pageIds.indexOf(page.id) > -1 && !page.trash
);
useEffect(() => {
setShowCreatePage(!resultsPageMeta.length);
//Determine whether to display the + New page
}, [resultsPageMeta, setShowCreatePage]);
return loading ? null : (
<>
{query ? (
resultsPageMeta.length ? (
<Command.Group
heading={t('Find results', { number: resultsPageMeta.length })}
>
{resultsPageMeta.map(result => {
return (
<Command.Item
key={result.id}
onSelect={() => {
onClose();
openPage(result.id);
}}
value={result.id}
>
<StyledListItem>
{result.mode === 'edgeless' ? (
<EdgelessIcon />
) : (
<PaperIcon />
)}
<span>{result.title}</span>
</StyledListItem>
</Command.Item>
);
})}
</Command.Group>
) : (
<StyledNotFound>
<span>{t('Find 0 result')}</span>
<NoResultSVG />
</StyledNotFound>
)
) : (
<Command.Group heading={t('Jump to')}>
{List.map(link => {
return (
<Command.Item
key={link.title}
value={link.title}
onSelect={() => {
onClose();
router.push(link.href);
}}
>
<StyledListItem>
<link.icon />
<span>{link.title}</span>
</StyledListItem>
</Command.Item>
);
})}
</Command.Group>
)}
</>
);
};

View File

@@ -0,0 +1,44 @@
import { FC, SVGProps } from 'react';
import {
AllPagesIcon,
FavouritesIcon,
TrashIcon,
SettingsIcon,
} from '@blocksuite/icons';
import { useTranslation } from '@affine/i18n';
export const useSwitchToConfig = (
currentWorkspaceId?: string
): {
title: string;
href: string;
icon: FC<SVGProps<SVGSVGElement>>;
}[] => {
const { t } = useTranslation();
return [
{
title: t('All pages'),
href: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/all` : '',
icon: AllPagesIcon,
},
{
title: t('Favorites'),
href: currentWorkspaceId
? `/workspace/${currentWorkspaceId}/favorite`
: '',
icon: FavouritesIcon,
},
{
title: t('Settings'),
href: currentWorkspaceId
? `/workspace/${currentWorkspaceId}/setting`
: '',
icon: SettingsIcon,
},
{
title: t('Trash'),
href: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/trash` : '',
icon: TrashIcon,
},
];
};

View File

@@ -0,0 +1,160 @@
import { Modal, ModalWrapper } from '@affine/component';
import {
StyledContent,
StyledModalHeader,
StyledModalFooter,
StyledModalDivider,
StyledShortcut,
} from './style';
import { Input } from './Input';
import { Results } from './Results';
import { Footer } from './Footer';
import { Command } from 'cmdk';
import { useEffect, useState } from 'react';
import { useModal } from '@/store/globalModal';
import { getUaHelper } from '@/utils';
import { useRouter } from 'next/router';
import { PublishedResults } from './PublishedResults';
type TransitionsModalProps = {
open: boolean;
onClose: () => void;
};
const isMac = () => {
return getUaHelper().isMacOs;
};
export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
const router = useRouter();
const [query, setQuery] = useState('');
const [loading, setLoading] = useState(true);
const [isPublic, setIsPublic] = useState(false);
const [publishWorkspaceName, setPublishWorkspaceName] = useState('');
const [showCreatePage, setShowCreatePage] = useState(true);
const { triggerQuickSearchModal } = useModal();
const isPublicAndNoQuery = () => {
return isPublic && query.length === 0;
};
const handleClose = () => {
setQuery('');
onClose();
};
// Add ‘⌘+K shortcut keys as switches
useEffect(() => {
if (router.pathname.startsWith('/404')) {
return;
}
const down = (e: KeyboardEvent) => {
if ((e.key === 'k' && e.metaKey) || (e.key === 'k' && e.ctrlKey)) {
const selection = window.getSelection();
setQuery('');
if (selection?.toString()) {
triggerQuickSearchModal(false);
return;
}
if (selection?.isCollapsed) {
triggerQuickSearchModal(!open);
}
}
};
document.addEventListener('keydown', down, { capture: true });
return () =>
document.removeEventListener('keydown', down, { capture: true });
}, [open, router, triggerQuickSearchModal]);
useEffect(() => {
if (router.pathname.startsWith('/public-workspace')) {
return setIsPublic(true);
} else {
return setIsPublic(false);
}
}, [router]);
useEffect(() => {
if (router.pathname.startsWith('/404')) {
return onClose();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Modal
open={open}
onClose={handleClose}
wrapperPosition={['top', 'center']}
data-testid="quickSearch"
>
<ModalWrapper
width={620}
style={{
maxHeight: '80vh',
minHeight: isPublicAndNoQuery() ? '72px' : '350px',
top: '12vh',
}}
>
<Command
shouldFilter={false}
//Handle KeyboardEvent conflicts with blocksuite
onKeyDown={(e: React.KeyboardEvent) => {
if (
e.key === 'ArrowDown' ||
e.key === 'ArrowUp' ||
e.key === 'ArrowLeft' ||
e.key === 'ArrowRight'
) {
e.stopPropagation();
}
}}
>
<StyledModalHeader>
<Input
open={open}
query={query}
setQuery={setQuery}
setLoading={setLoading}
isPublic={isPublic}
publishWorkspaceName={publishWorkspaceName}
/>
<StyledShortcut>{isMac() ? '⌘ + K' : 'Ctrl + K'}</StyledShortcut>
</StyledModalHeader>
<StyledModalDivider
style={{ display: isPublicAndNoQuery() ? 'none' : '' }}
/>
<Command.List>
<StyledContent
style={{ display: isPublicAndNoQuery() ? 'none' : '' }}
>
{!isPublic ? (
<Results
query={query}
loading={loading}
setLoading={setLoading}
onClose={handleClose}
setShowCreatePage={setShowCreatePage}
/>
) : (
<PublishedResults
query={query}
loading={loading}
setLoading={setLoading}
onClose={handleClose}
setPublishWorkspaceName={setPublishWorkspaceName}
data-testid="publishedSearchResults"
/>
)}
</StyledContent>
{!isPublic ? (
showCreatePage ? (
<>
<StyledModalDivider />
<StyledModalFooter>
<Footer query={query} onClose={handleClose} />
</StyledModalFooter>
</>
) : null
) : null}
</Command.List>
</Command>
</ModalWrapper>
</Modal>
);
};
export default QuickSearch;

View File

@@ -0,0 +1,158 @@
import { displayFlex, styled } from '@affine/component';
export const StyledContent = styled('div')(({ theme }) => {
return {
minHeight: '220px',
maxHeight: '55vh',
width: '100%',
overflow: 'auto',
marginBottom: '10px',
...displayFlex('center', 'flex-start'),
color: theme.colors.popoverColor,
transition: 'all 0.15s',
letterSpacing: '0.06em',
'[cmdk-group-heading]': {
margin: '5px 16px',
fontSize: theme.font.base,
fontWeight: '500',
},
'[aria-selected="true"]': {
borderRadius: '5px',
color: theme.colors.primaryColor,
backgroundColor: theme.colors.hoverBackground,
},
};
});
export const StyledJumpTo = styled('div')(({ theme }) => {
return {
...displayFlex('center', 'start'),
flexDirection: 'column',
padding: '10px 10px 10px 0',
fontSize: theme.font.base,
strong: {
fontWeight: '500',
marginBottom: '10px',
},
};
});
export const StyledNotFound = styled('div')(({ theme }) => {
return {
width: '612px',
...displayFlex('center', 'center'),
flexDirection: 'column',
padding: '5px 16px',
fontSize: theme.font.base,
span: {
width: '100%',
fontWeight: '500',
},
'>svg': {
marginTop: '10px',
fontSize: '150px',
},
};
});
export const StyledInputContent = styled('div')(({ theme }) => {
return {
margin: '13px 0',
...displayFlex('space-between', 'center'),
input: {
width: '492px',
height: '22px',
padding: '0 12px',
fontSize: theme.font.base,
...displayFlex('space-between', 'center'),
letterSpacing: '0.06em',
color: theme.colors.popoverColor,
'::placeholder': {
color: theme.colors.placeHolderColor,
},
},
};
});
export const StyledShortcut = styled('div')(({ theme }) => {
return {
color: theme.colors.placeHolderColor,
fontSize: theme.font.base,
whiteSpace: 'nowrap',
};
});
export const StyledLabel = styled('label')(({ theme }) => {
return {
width: '24px',
height: '24px',
color: theme.colors.iconColor,
};
});
export const StyledModalHeader = styled('div')(({ theme }) => {
return {
height: '48px',
margin: '12px 24px 0px 24px',
...displayFlex('space-between', 'center'),
color: theme.colors.popoverColor,
};
});
export const StyledModalDivider = styled('div')(({ theme }) => {
return {
width: 'auto',
height: '0',
margin: '6px 16px 6.5px 16px',
position: 'relative',
borderTop: `0.5px solid ${theme.colors.placeHolderColor}`,
transition: 'all 0.15s',
};
});
export const StyledModalFooter = styled('div')(({ theme }) => {
return {
fontSize: theme.font.base,
lineHeight: '22px',
marginBottom: '8px',
textAlign: 'center',
...displayFlex('center', 'center'),
color: theme.colors.popoverColor,
'[aria-selected="true"]': {
transition: 'background .15s, color .15s',
borderRadius: '5px',
color: theme.colors.primaryColor,
backgroundColor: theme.colors.hoverBackground,
},
};
});
export const StyledModalFooterContent = styled.button(({ theme }) => {
return {
width: '612px',
height: '32px',
fontSize: theme.font.base,
lineHeight: '22px',
textAlign: 'center',
...displayFlex('center', 'center'),
color: 'inherit',
borderRadius: '5px',
transition: 'background .15s, color .15s',
'>svg': {
fontSize: '20px',
marginRight: '12px',
},
};
});
export const StyledListItem = styled.button(({ theme }) => {
return {
width: '612px',
height: '32px',
fontSize: theme.font.base,
color: 'inherit',
paddingLeft: '12px',
borderRadius: '5px',
transition: 'background .15s, color .15s',
...displayFlex('flex-start', 'center'),
'>svg': {
fontSize: '20px',
marginRight: '12px',
},
};
});

View File

@@ -0,0 +1,27 @@
export const CloseIcon = () => {
return (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M7.94 7.00014L13.4667 1.47348C13.5759 1.34594 13.633 1.18189 13.6265 1.01411C13.62 0.846324 13.5504 0.687165 13.4317 0.568435C13.313 0.449706 13.1538 0.38015 12.986 0.37367C12.8183 0.367189 12.6542 0.42426 12.5267 0.533477L7 6.06014L1.47334 0.526811C1.3478 0.401275 1.17754 0.33075 1 0.33075C0.822468 0.33075 0.652205 0.401275 0.526669 0.526811C0.401133 0.652346 0.330608 0.82261 0.330608 1.00014C0.330608 1.17768 0.401133 1.34794 0.526669 1.47348L6.06 7.00014L0.526669 12.5268C0.456881 12.5866 0.400201 12.6601 0.360186 12.7428C0.32017 12.8255 0.297683 12.9156 0.294137 13.0074C0.290591 13.0993 0.306061 13.1908 0.339577 13.2764C0.373094 13.3619 0.423932 13.4396 0.488902 13.5046C0.553872 13.5695 0.63157 13.6204 0.71712 13.6539C0.80267 13.6874 0.894225 13.7029 0.986038 13.6993C1.07785 13.6958 1.16794 13.6733 1.25065 13.6333C1.33336 13.5933 1.4069 13.5366 1.46667 13.4668L7 7.94014L12.5267 13.4668C12.6542 13.576 12.8183 13.6331 12.986 13.6266C13.1538 13.6201 13.313 13.5506 13.4317 13.4319C13.5504 13.3131 13.62 13.154 13.6265 12.9862C13.633 12.8184 13.5759 12.6543 13.4667 12.5268L7.94 7.00014Z" />
</svg>
);
};
export const KeyboardIcon = () => {
return (
<svg
width="20"
height="15"
viewBox="0 0 20 15"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M17.745 0C18.3417 0 18.914 0.237053 19.336 0.65901C19.7579 1.08097 19.995 1.65326 19.995 2.25V11.755C19.995 12.3517 19.7579 12.924 19.336 13.346C18.914 13.7679 18.3417 14.005 17.745 14.005H2.25C1.95453 14.005 1.66194 13.9468 1.38896 13.8337C1.11598 13.7207 0.867941 13.5549 0.65901 13.346C0.450078 13.1371 0.284344 12.889 0.171271 12.616C0.058198 12.3431 0 12.0505 0 11.755V2.25C0 1.65326 0.237053 1.08097 0.65901 0.65901C1.08097 0.237053 1.65326 0 2.25 0H17.745ZM17.745 1.5H2.25C2.05109 1.5 1.86032 1.57902 1.71967 1.71967C1.57902 1.86032 1.5 2.05109 1.5 2.25V11.755C1.5 12.169 1.836 12.505 2.25 12.505H17.745C17.9439 12.505 18.1347 12.426 18.2753 12.2853C18.416 12.1447 18.495 11.9539 18.495 11.755V2.25C18.495 2.05109 18.416 1.86032 18.2753 1.71967C18.1347 1.57902 17.9439 1.5 17.745 1.5ZM4.75 9.5H15.25C15.44 9.50006 15.6229 9.57224 15.7618 9.70197C15.9006 9.8317 15.9851 10.0093 15.998 10.1989C16.011 10.3885 15.9515 10.5759 15.8316 10.7233C15.7117 10.8707 15.5402 10.9671 15.352 10.993L15.25 11H4.75C4.55998 10.9999 4.37706 10.9278 4.23821 10.798C4.09936 10.6683 4.01493 10.4907 4.00197 10.3011C3.98902 10.1115 4.04852 9.92411 4.16843 9.7767C4.28835 9.62929 4.45975 9.5329 4.648 9.507L4.75 9.5H15.25H4.75ZM14.5 6C14.7652 6 15.0196 6.10536 15.2071 6.29289C15.3946 6.48043 15.5 6.73478 15.5 7C15.5 7.26522 15.3946 7.51957 15.2071 7.70711C15.0196 7.89464 14.7652 8 14.5 8C14.2348 8 13.9804 7.89464 13.7929 7.70711C13.6054 7.51957 13.5 7.26522 13.5 7C13.5 6.73478 13.6054 6.48043 13.7929 6.29289C13.9804 6.10536 14.2348 6 14.5 6ZM8.505 6C8.77022 6 9.02457 6.10536 9.21211 6.29289C9.39964 6.48043 9.505 6.73478 9.505 7C9.505 7.26522 9.39964 7.51957 9.21211 7.70711C9.02457 7.89464 8.77022 8 8.505 8C8.23978 8 7.98543 7.89464 7.79789 7.70711C7.61036 7.51957 7.505 7.26522 7.505 7C7.505 6.73478 7.61036 6.48043 7.79789 6.29289C7.98543 6.10536 8.23978 6 8.505 6ZM5.505 6C5.77022 6 6.02457 6.10536 6.21211 6.29289C6.39964 6.48043 6.505 6.73478 6.505 7C6.505 7.26522 6.39964 7.51957 6.21211 7.70711C6.02457 7.89464 5.77022 8 5.505 8C5.23978 8 4.98543 7.89464 4.79789 7.70711C4.61036 7.51957 4.505 7.26522 4.505 7C4.505 6.73478 4.61036 6.48043 4.79789 6.29289C4.98543 6.10536 5.23978 6 5.505 6ZM11.505 6C11.7702 6 12.0246 6.10536 12.2121 6.29289C12.3996 6.48043 12.505 6.73478 12.505 7C12.505 7.26522 12.3996 7.51957 12.2121 7.70711C12.0246 7.89464 11.7702 8 11.505 8C11.2398 8 10.9854 7.89464 10.7979 7.70711C10.6104 7.51957 10.505 7.26522 10.505 7C10.505 6.73478 10.6104 6.48043 10.7979 6.29289C10.9854 6.10536 11.2398 6 11.505 6ZM4 3C4.26522 3 4.51957 3.10536 4.70711 3.29289C4.89464 3.48043 5 3.73478 5 4C5 4.26522 4.89464 4.51957 4.70711 4.70711C4.51957 4.89464 4.26522 5 4 5C3.73478 5 3.48043 4.89464 3.29289 4.70711C3.10536 4.51957 3 4.26522 3 4C3 3.73478 3.10536 3.48043 3.29289 3.29289C3.48043 3.10536 3.73478 3 4 3ZM6.995 3C7.26022 3 7.51457 3.10536 7.70211 3.29289C7.88964 3.48043 7.995 3.73478 7.995 4C7.995 4.26522 7.88964 4.51957 7.70211 4.70711C7.51457 4.89464 7.26022 5 6.995 5C6.72978 5 6.47543 4.89464 6.28789 4.70711C6.10036 4.51957 5.995 4.26522 5.995 4C5.995 3.73478 6.10036 3.48043 6.28789 3.29289C6.47543 3.10536 6.72978 3 6.995 3ZM9.995 3C10.2602 3 10.5146 3.10536 10.7021 3.29289C10.8896 3.48043 10.995 3.73478 10.995 4C10.995 4.26522 10.8896 4.51957 10.7021 4.70711C10.5146 4.89464 10.2602 5 9.995 5C9.72978 5 9.47543 4.89464 9.28789 4.70711C9.10036 4.51957 8.995 4.26522 8.995 4C8.995 3.73478 9.10036 3.48043 9.28789 3.29289C9.47543 3.10536 9.72978 3 9.995 3ZM12.995 3C13.2602 3 13.5146 3.10536 13.7021 3.29289C13.8896 3.48043 13.995 3.73478 13.995 4C13.995 4.26522 13.8896 4.51957 13.7021 4.70711C13.5146 4.89464 13.2602 5 12.995 5C12.7298 5 12.4754 4.89464 12.2879 4.70711C12.1004 4.51957 11.995 4.26522 11.995 4C11.995 3.73478 12.1004 3.48043 12.2879 3.29289C12.4754 3.10536 12.7298 3 12.995 3ZM15.995 3C16.2602 3 16.5146 3.10536 16.7021 3.29289C16.8896 3.48043 16.995 3.73478 16.995 4C16.995 4.26522 16.8896 4.51957 16.7021 4.70711C16.5146 4.89464 16.2602 5 15.995 5C15.7298 5 15.4754 4.89464 15.2879 4.70711C15.1004 4.51957 14.995 4.26522 14.995 4C14.995 3.73478 15.1004 3.48043 15.2879 3.29289C15.4754 3.10536 15.7298 3 15.995 3Z" />
</svg>
);
};

View File

@@ -0,0 +1,90 @@
import { useTranslation } from '@affine/i18n';
interface ShortcutTip {
[x: string]: string;
}
export const useMacKeyboardShortcuts = (): ShortcutTip => {
const { t } = useTranslation();
return {
[t('Undo')]: '⌘+Z',
[t('Redo')]: '⌘+⇧+Z',
[t('Bold')]: '⌘+B',
[t('Italic')]: '⌘+I',
[t('Underline')]: '⌘+U',
[t('Strikethrough')]: '⌘+⇧+S',
[t('Inline code')]: ' ⌘+E',
[t('Code block')]: '⌘+⌥+C',
[t('Link')]: '⌘+K',
[t('Quick search')]: '⌘+K',
[t('Body text')]: '⌘+⌥+0',
[t('Heading', { number: '1' })]: '⌘+⌥+1',
[t('Heading', { number: '2' })]: '⌘+⌥+2',
[t('Heading', { number: '3' })]: '⌘+⌥+3',
[t('Heading', { number: '4' })]: '⌘+⌥+4',
[t('Heading', { number: '5' })]: '⌘+⌥+5',
[t('Heading', { number: '6' })]: '⌘+⌥+6',
[t('Increase indent')]: 'Tab',
[t('Reduce indent')]: '⇧+Tab',
};
};
export const useMacMarkdownShortcuts = (): ShortcutTip => {
const { t } = useTranslation();
return {
[t('Bold')]: '**Text** ',
[t('Italic')]: '*Text* ',
[t('Underline')]: '~Text~ ',
[t('Strikethrough')]: '~~Text~~ ',
[t('Divider')]: '***',
[t('Inline code')]: '`Text` ',
[t('Code block')]: '``` Space',
[t('Heading', { number: '1' })]: '# Text',
[t('Heading', { number: '2' })]: '## Text',
[t('Heading', { number: '3' })]: '### Text',
[t('Heading', { number: '4' })]: '#### Text',
[t('Heading', { number: '5' })]: '##### Text',
[t('Heading', { number: '6' })]: '###### Text',
};
};
export const useWindowsKeyboardShortcuts = (): ShortcutTip => {
const { t } = useTranslation();
return {
[t('Undo')]: 'Ctrl+Z',
[t('Redo')]: 'Ctrl+Y',
[t('Bold')]: 'Ctrl+B',
[t('Italic')]: 'Ctrl+I',
[t('Underline')]: 'Ctrl+U',
[t('Strikethrough')]: 'Ctrl+Shift+S',
[t('Inline code')]: ' Ctrl+E',
[t('Code block')]: 'Ctrl+Alt+C',
[t('Link')]: 'Ctrl+K',
[t('Quick search')]: 'Ctrl+K',
[t('Body text')]: 'Ctrl+Shift+0',
[t('Heading', { number: '1' })]: 'Ctrl+Shift+1',
[t('Heading', { number: '2' })]: 'Ctrl+Shift+2',
[t('Heading', { number: '3' })]: 'Ctrl+Shift+3',
[t('Heading', { number: '4' })]: 'Ctrl+Shift+4',
[t('Heading', { number: '5' })]: 'Ctrl+Shift+5',
[t('Heading', { number: '6' })]: 'Ctrl+Shift+6',
[t('Increase indent')]: 'Tab',
[t('Reduce indent')]: 'Shift+Tab',
};
};
export const useWinMarkdownShortcuts = (): ShortcutTip => {
const { t } = useTranslation();
return {
[t('Bold')]: '**Text** ',
[t('Italic')]: '*Text* ',
[t('Underline')]: '~Text~ ',
[t('Strikethrough')]: '~~Text~~ ',
[t('Divider')]: '***',
[t('Inline code')]: '`Text` ',
[t('Code block')]: '``` Text',
[t('Heading', { number: '1' })]: '# Text',
[t('Heading', { number: '2' })]: '## Text',
[t('Heading', { number: '3' })]: '### Text',
[t('Heading', { number: '4' })]: '#### Text',
[t('Heading', { number: '5' })]: '##### Text',
[t('Heading', { number: '6' })]: '###### Text',
};
};

View File

@@ -0,0 +1,89 @@
import { createPortal } from 'react-dom';
import { KeyboardIcon } from './Icons';
import {
StyledListItem,
StyledModalHeader,
StyledShortcutsModal,
StyledSubTitle,
StyledTitle,
} from './style';
import {
useMacKeyboardShortcuts,
useMacMarkdownShortcuts,
useWindowsKeyboardShortcuts,
useWinMarkdownShortcuts,
} from '@/components/shortcuts-modal/config';
import { MuiSlide } from '@affine/component';
import { ModalCloseButton } from '@affine/component';
import { getUaHelper } from '@/utils';
import { useTranslation } from '@affine/i18n';
type ModalProps = {
open: boolean;
onClose: () => void;
};
const isMac = () => {
return getUaHelper().isMacOs;
};
export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
const { t } = useTranslation();
const macMarkdownShortcuts = useMacMarkdownShortcuts();
const winMarkdownShortcuts = useWinMarkdownShortcuts();
const macKeyboardShortcuts = useMacKeyboardShortcuts();
const windowsKeyboardShortcuts = useWindowsKeyboardShortcuts();
const markdownShortcuts = isMac()
? macMarkdownShortcuts
: winMarkdownShortcuts;
const keyboardShortcuts = isMac()
? macKeyboardShortcuts
: windowsKeyboardShortcuts;
return createPortal(
<MuiSlide direction="left" in={open} mountOnEnter unmountOnExit>
<StyledShortcutsModal data-testid="shortcuts-modal">
<>
<StyledModalHeader>
<StyledTitle>
<KeyboardIcon />
{t('Shortcuts')}
</StyledTitle>
<ModalCloseButton
top={6}
right={6}
size={[24, 24]}
iconSize={[15, 15]}
onClick={() => {
onClose();
}}
/>
</StyledModalHeader>
<StyledSubTitle style={{ marginTop: 0 }}>
{t('Keyboard Shortcuts')}
</StyledSubTitle>
{Object.entries(keyboardShortcuts).map(([title, shortcuts]) => {
return (
<StyledListItem key={title}>
<span>{title}</span>
<span>{shortcuts}</span>
</StyledListItem>
);
})}
<StyledSubTitle>{t('Markdown Syntax')}</StyledSubTitle>
{Object.entries(markdownShortcuts).map(([title, shortcuts]) => {
return (
<StyledListItem key={title}>
<span>{title}</span>
<span>{shortcuts}</span>
</StyledListItem>
);
})}
</>
</StyledShortcutsModal>
</MuiSlide>,
document.body
);
};
export default ShortcutsModal;

View File

@@ -0,0 +1,59 @@
import { displayFlex, styled } from '@affine/component';
export const StyledShortcutsModal = styled.div(({ theme }) => ({
width: '288px',
height: '74vh',
paddingBottom: '28px',
backgroundColor: theme.colors.popoverBackground,
boxShadow: theme.shadow.popover,
borderRadius: `${theme.radius.popover} 0 ${theme.radius.popover} ${theme.radius.popover}`,
color: theme.colors.popoverColor,
overflow: 'auto',
boxRadius: '10px',
position: 'fixed',
right: '12px',
top: '0',
bottom: '0',
margin: 'auto',
zIndex: theme.zIndex.modal,
}));
export const StyledTitle = styled.div(({ theme }) => ({
color: theme.colors.textColor,
fontWeight: '500',
fontSize: theme.font.sm,
height: '44px',
...displayFlex('center', 'center'),
svg: {
width: '20px',
marginRight: '14px',
color: theme.colors.primaryColor,
},
}));
export const StyledSubTitle = styled.div(({ theme }) => ({
color: theme.colors.popoverColor,
fontWeight: '500',
fontSize: theme.font.sm,
height: '34px',
lineHeight: '36px',
marginTop: '28px',
padding: '0 16px',
}));
export const StyledModalHeader = styled.div(() => ({
...displayFlex('space-between', 'center'),
paddingTop: '8px 4px 0 4px',
width: '100%',
padding: '8px 16px 0 16px',
position: 'sticky',
left: '0',
top: '0',
background: 'var(--affine-popover-background)',
transition: 'background-color 0.5s',
}));
export const StyledListItem = styled.div(({ theme }) => ({
height: '34px',
...displayFlex('space-between', 'center'),
fontSize: theme.font.sm,
padding: '0 16px',
}));

View File

@@ -0,0 +1,59 @@
import { LitElement, css, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import * as React from 'react';
export const tagName = 'simple-counter';
// Adapt React in order to be able to use custom tags properly
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace JSX {
interface IntrinsicElements {
[tagName]: PersonInfoProps;
}
}
}
interface PersonInfoProps
extends React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement>,
HTMLElement
> {
name?: string;
}
// ===================== Adapt end ====================
@customElement(tagName)
export class Counter extends LitElement {
static styles = css`
.counter-container {
display: flex;
color: var(--affine-text-color);
}
button {
margin: 0 5px;
}
`;
@property()
name?: string = '';
@state()
count = 0;
// Render the UI as a function of component state
render() {
return html`<div class="counter-container">
<div class="name">${this.name}</div>
<button @click=${this._subtract}>-</button>
<div>${this.count}</div>
<button @click="${this._increment}">+</button>
</div>`;
}
private _increment() {
this.count++;
}
private _subtract() {
this.count--;
}
}

View File

@@ -0,0 +1,59 @@
import { stringToColour } from '@/utils';
interface IWorkspaceAvatar {
size: number;
name: string;
avatar: string;
style?: React.CSSProperties;
}
export const WorkspaceAvatar = (props: IWorkspaceAvatar) => {
const size = props.size || 20;
const sizeStr = size + 'px';
return (
<>
{props.avatar ? (
<div
style={{
...props.style,
width: sizeStr,
height: sizeStr,
border: '1px solid #fff',
color: '#fff',
borderRadius: '50%',
overflow: 'hidden',
display: 'inline-block',
verticalAlign: 'middle',
}}
>
<picture>
<img
style={{ width: sizeStr, height: sizeStr }}
src={props.avatar}
alt=""
/>
</picture>
</div>
) : (
<div
style={{
...props.style,
width: sizeStr,
height: sizeStr,
border: '1px solid #fff',
color: '#fff',
fontSize: Math.ceil(0.5 * size) + 'px',
background: stringToColour(props.name || 'AFFiNE'),
borderRadius: '50%',
textAlign: 'center',
lineHeight: size + 'px',
display: 'inline-block',
verticalAlign: 'middle',
}}
>
{(props.name || 'AFFiNE').substring(0, 1)}
</div>
)}
</>
);
};

View File

@@ -0,0 +1,43 @@
import { useState, useEffect } from 'react';
import type { WorkspaceUnit } from '@affine/datacenter';
import { WorkspaceAvatar as Avatar } from './Avatar';
const useAvatar = (workspaceUnit: WorkspaceUnit | null) => {
const [avatarUrl, setAvatarUrl] = useState('');
const avatarId =
workspaceUnit?.avatar || workspaceUnit?.blocksuiteWorkspace?.meta.avatar;
const blobs = workspaceUnit?.blocksuiteWorkspace?.blobs;
useEffect(() => {
if (avatarId && blobs) {
blobs.then(blobs => {
blobs?.get(avatarId).then(url => setAvatarUrl(url || ''));
});
} else {
setAvatarUrl('');
}
}, [avatarId, blobs]);
return avatarUrl;
};
export const WorkspaceUnitAvatar = ({
size = 20,
name,
workspaceUnit = null,
style,
}: {
size?: number;
name?: string;
workspaceUnit?: WorkspaceUnit | null;
style?: React.CSSProperties;
}) => {
const avatarUrl = useAvatar(workspaceUnit);
return (
<Avatar
size={size}
name={name || workspaceUnit?.name || ''}
avatar={avatarUrl}
style={style}
/>
);
};

View File

@@ -0,0 +1,2 @@
export { WorkspaceAvatar } from './Avatar';
export { WorkspaceUnitAvatar } from './WorkspaceUnitAvatar';

View File

@@ -0,0 +1,42 @@
import HelpIsland from '@/components/help-island';
import { WorkSpaceSliderBar } from '@/components/workspace-slider-bar';
import { useRouter } from 'next/router';
import { StyledPage, StyledToolWrapper, StyledWrapper } from './styles';
import { PropsWithChildren } from 'react';
import useEnsureWorkspace from '@/hooks/use-ensure-workspace';
import { PageLoading } from '@/components/loading';
export const WorkspaceDefender = ({ children }: PropsWithChildren) => {
const { workspaceLoaded } = useEnsureWorkspace();
return <>{workspaceLoaded ? children : <PageLoading />}</>;
};
export const WorkspaceLayout = ({ children }: PropsWithChildren) => {
const router = useRouter();
return (
<StyledPage>
<WorkSpaceSliderBar />
<StyledWrapper>
{children}
<StyledToolWrapper>
<div id="toolWrapper" style={{ marginBottom: '12px' }}>
{/* Slot for block hub */}
</div>
<HelpIsland
showList={router.query.pageId ? undefined : ['contact']}
/>
</StyledToolWrapper>
</StyledWrapper>
</StyledPage>
);
};
export const Layout = ({ children }: PropsWithChildren) => {
return (
<WorkspaceDefender>
<WorkspaceLayout>{children}</WorkspaceLayout>
</WorkspaceDefender>
);
};
export default Layout;

View File

@@ -0,0 +1,27 @@
import { styled } from '@affine/component';
export const StyledPage = styled('div')(({ theme }) => {
return {
height: '100vh',
backgroundColor: theme.colors.pageBackground,
transition: 'background-color .5s',
display: 'flex',
flexGrow: '1',
};
});
export const StyledWrapper = styled('div')(() => {
return {
flexGrow: 1,
position: 'relative',
};
});
export const StyledToolWrapper = styled('div')(({ theme }) => {
return {
position: 'fixed',
right: '30px',
bottom: '30px',
zIndex: theme.zIndex.popover,
};
});

View File

@@ -0,0 +1,64 @@
import { CloudInsyncIcon, LogOutIcon } from '@blocksuite/icons';
import { FlexWrapper } from '@affine/component';
import { WorkspaceAvatar } from '@/components/workspace-avatar';
import { IconButton } from '@affine/component';
import { useAppState } from '@/providers/app-state-provider';
import { StyledFooter, StyleUserInfo, StyledSignInButton } from './styles';
import { useTranslation } from '@affine/i18n';
import { Tooltip } from '@affine/component';
export const Footer = ({
onLogin,
onLogout,
}: {
onLogin: () => void;
onLogout: () => void;
}) => {
const { user } = useAppState();
const { t } = useTranslation();
return (
<StyledFooter>
{user && (
<>
<FlexWrapper>
<WorkspaceAvatar
size={40}
name={user.name}
avatar={user.avatar}
></WorkspaceAvatar>
<StyleUserInfo>
<p>{user.name}</p>
<p>{user.email}</p>
</StyleUserInfo>
</FlexWrapper>
<Tooltip content={t('Sign out')} disablePortal={true}>
<IconButton
onClick={() => {
onLogout();
}}
>
<LogOutIcon />
</IconButton>
</Tooltip>
</>
)}
{!user && (
<StyledSignInButton
noBorder
bold
icon={
<div className="circle">
<CloudInsyncIcon fontSize={16} />
</div>
}
onClick={async () => {
onLogin();
}}
>
{t('Sign in')}
</StyledSignInButton>
)}
</StyledFooter>
);
};

View File

@@ -0,0 +1,62 @@
import { LOCALES } from '@affine/i18n';
import { styled } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { ArrowDownIcon } from '@blocksuite/icons';
import { Button } from '@affine/component';
import { Menu, MenuItem } from '@affine/component';
const LanguageMenuContent = () => {
const { i18n } = useTranslation();
const changeLanguage = (event: string) => {
i18n.changeLanguage(event);
};
return (
<>
{LOCALES.map(option => {
return (
<ListItem
key={option.name}
title={option.name}
onClick={() => {
changeLanguage(option.tag);
}}
>
{option.originalName}
</ListItem>
);
})}
</>
);
};
export const LanguageMenu = () => {
const { i18n } = useTranslation();
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);
return (
<Menu
content={<LanguageMenuContent />}
placement="bottom"
trigger="click"
disablePortal={true}
>
<Button
icon={<ArrowDownIcon />}
iconPosition="end"
noBorder={true}
style={{ textTransform: 'capitalize' }}
data-testid="language-menu-button"
>
{currentLanguage?.originalName}
</Button>
</Menu>
);
};
const ListItem = styled(MenuItem)(({ theme }) => ({
height: '38px',
color: theme.colors.popoverColor,
fontSize: theme.font.sm,
textTransform: 'capitalize',
padding: '0 24px',
}));

View File

@@ -0,0 +1,83 @@
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import {
CloudIcon,
LocalIcon,
OfflineIcon,
PublishedIcon,
} from '@/components/workspace-modal/icons';
import { UsersIcon } from '@blocksuite/icons';
import { WorkspaceUnit } from '@affine/datacenter';
import { useAppState } from '@/providers/app-state-provider';
import { StyleWorkspaceInfo, StyleWorkspaceTitle, StyledCard } from './styles';
import { useTranslation } from '@affine/i18n';
import { FlexWrapper } from '@affine/component';
const WorkspaceType = ({ workspaceData }: { workspaceData: WorkspaceUnit }) => {
const { user } = useAppState();
const { t } = useTranslation();
const isOwner = user?.id === workspaceData.owner?.id;
if (workspaceData.provider === 'local') {
return (
<p>
<LocalIcon />
{t('Local Workspace')}
</p>
);
}
return isOwner ? (
<p>
<CloudIcon />
{t('Cloud Workspace')}
</p>
) : (
<p>
<UsersIcon fontSize={20} color={'#FF646B'} />
{t('Joined Workspace')}
</p>
);
};
export const WorkspaceCard = ({
workspaceData,
onClick,
}: {
workspaceData: WorkspaceUnit;
onClick: (data: WorkspaceUnit) => void;
}) => {
const { currentWorkspace } = useAppState();
const { t } = useTranslation();
return (
<StyledCard
data-testid="workspace-card"
onClick={() => {
onClick(workspaceData);
}}
active={workspaceData.id === currentWorkspace?.id}
>
<FlexWrapper>
<WorkspaceUnitAvatar size={58} workspaceUnit={workspaceData} />
</FlexWrapper>
<StyleWorkspaceInfo>
<StyleWorkspaceTitle>
{workspaceData.name || 'AFFiNE'}
</StyleWorkspaceTitle>
<WorkspaceType workspaceData={workspaceData} />
{workspaceData.provider === 'local' && (
<p>
<OfflineIcon />
{t('Available Offline')}
</p>
)}
{workspaceData.published && (
<p>
<PublishedIcon />
{t('Published to Web')}
</p>
)}
</StyleWorkspaceInfo>
</StyledCard>
);
};

View File

@@ -0,0 +1,99 @@
export const LocalIcon = () => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.86315 3.54163H13.1382C13.738 3.54162 14.2261 3.54161 14.6222 3.57398C15.0314 3.60741 15.3972 3.67848 15.7377 3.85196C16.2734 4.12493 16.709 4.5605 16.982 5.09624C17.1555 5.43671 17.2265 5.80252 17.26 6.21174C17.2923 6.60785 17.2923 7.09591 17.2923 7.69577V10.9153C17.2923 11.5151 17.2923 12.0032 17.26 12.3993C17.2265 12.8085 17.1555 13.1743 16.982 13.5148C16.709 14.0505 16.2734 14.4861 15.7377 14.7591C15.3972 14.9326 15.0314 15.0036 14.6222 15.0371C14.2261 15.0694 13.738 15.0694 13.1382 15.0694H12.8479V16.0416H13.7044C14.0495 16.0416 14.3294 16.3214 14.3294 16.6666C14.3294 17.0118 14.0495 17.2916 13.7044 17.2916H6.29695C5.95177 17.2916 5.67195 17.0118 5.67195 16.6666C5.67195 16.3214 5.95177 16.0416 6.29695 16.0416H7.15343V15.0694H6.86313C6.26327 15.0694 5.77521 15.0694 5.37909 15.0371C4.96988 15.0036 4.60407 14.9326 4.2636 14.7591C3.72786 14.4861 3.29229 14.0505 3.01931 13.5148C2.84583 13.1743 2.77477 12.8085 2.74134 12.3993C2.70897 12.0032 2.70898 11.5151 2.70898 10.9152V7.69579C2.70898 7.09592 2.70897 6.60786 2.74134 6.21174C2.77477 5.80252 2.84583 5.43671 3.01931 5.09624C3.29229 4.5605 3.72786 4.12493 4.2636 3.85196C4.60407 3.67848 4.96988 3.60741 5.37909 3.57398C5.77521 3.54161 6.26328 3.54162 6.86315 3.54163ZM3.96013 11.4583C3.96232 11.801 3.96868 12.071 3.98719 12.2975C4.0143 12.6294 4.06434 12.8124 4.13307 12.9473C4.2862 13.2478 4.53055 13.4922 4.83108 13.6453C4.96597 13.714 5.14897 13.7641 5.48088 13.7912C5.82009 13.8189 6.25695 13.8194 6.88954 13.8194H13.1118C13.7444 13.8194 14.1812 13.8189 14.5204 13.7912C14.8523 13.7641 15.0353 13.714 15.1702 13.6453C15.4708 13.4922 15.7151 13.2478 15.8682 12.9473C15.937 12.8124 15.987 12.6294 16.0141 12.2975C16.0326 12.071 16.039 11.801 16.0412 11.4583H3.96013ZM16.0423 10.2083H3.95898V7.72218C3.95898 7.08959 3.95947 6.65273 3.98719 6.31353C4.0143 5.98162 4.06434 5.79861 4.13307 5.66372C4.2862 5.36319 4.53055 5.11884 4.83108 4.96571C4.96597 4.89698 5.14897 4.84694 5.48088 4.81983C5.82009 4.79211 6.25695 4.79163 6.88954 4.79163H13.1118C13.7444 4.79163 14.1812 4.79211 14.5204 4.81983C14.8523 4.84694 15.0353 4.89698 15.1702 4.96571C15.4708 5.11884 15.7151 5.36319 15.8682 5.66372C15.937 5.79861 15.987 5.98162 16.0141 6.31353C16.0418 6.65273 16.0423 7.08959 16.0423 7.72218V10.2083ZM11.5979 15.0694H8.40343V16.0416H11.5979V15.0694ZM10.0007 11.9583C10.3458 11.9583 10.6257 12.2381 10.6257 12.5833V12.5916C10.6257 12.9368 10.3458 13.2166 10.0007 13.2166C9.65547 13.2166 9.37565 12.9368 9.37565 12.5916V12.5833C9.37565 12.2381 9.65547 11.9583 10.0007 11.9583Z"
fill="#FDBD32"
/>
</svg>
);
};
export const OfflineIcon = () => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.12249 3.54165C8.47134 3.54145 8.74594 3.54129 9.01129 3.60499C9.24512 3.66113 9.46866 3.75372 9.6737 3.87937C9.90638 4.02195 10.1004 4.21624 10.347 4.46306L10.4942 4.61035C10.8035 4.91964 10.889 4.99941 10.9794 5.05484C11.0726 5.11195 11.1742 5.15404 11.2805 5.17956C11.3837 5.20432 11.5005 5.20834 11.9379 5.20834L14.8587 5.20834C15.3038 5.20833 15.6754 5.20832 15.9789 5.23312C16.2955 5.25899 16.5927 5.31491 16.8737 5.45812C17.3049 5.67783 17.6555 6.02841 17.8752 6.45961C18.0184 6.74066 18.0744 7.03788 18.1002 7.35445C18.125 7.65797 18.125 8.02951 18.125 8.47463V13.192C18.125 13.6372 18.125 14.0087 18.1002 14.3122C18.0744 14.6288 18.0184 14.926 17.8752 15.2071C17.6555 15.6383 17.3049 15.9889 16.8737 16.2086C16.5927 16.3518 16.2955 16.4077 15.9789 16.4336C15.6754 16.4584 15.3039 16.4583 14.8587 16.4583H5.14129C4.69618 16.4583 4.32463 16.4584 4.02111 16.4336C3.70454 16.4077 3.40732 16.3518 3.12627 16.2086C2.69507 15.9889 2.34449 15.6383 2.12478 15.2071C1.98157 14.926 1.92565 14.6288 1.89978 14.3122C1.87498 14.0087 1.87499 13.6372 1.875 13.192V6.80798C1.87499 6.36285 1.87498 5.99131 1.89978 5.68778C1.92565 5.37121 1.98157 5.074 2.12478 4.79294C2.34449 4.36174 2.69507 4.01116 3.12627 3.79145C3.40732 3.64825 3.70454 3.59232 4.02111 3.56645C4.32464 3.54166 4.69618 3.54166 5.14131 3.54167L8.12249 3.54165ZM3.125 13.1667V6.83334C3.125 6.69601 3.12504 6.57168 3.12558 6.45836L14.8333 6.45834C15.3104 6.45834 15.6305 6.45882 15.8771 6.47897C16.1164 6.49852 16.2308 6.53342 16.3062 6.57187C16.5022 6.67174 16.6616 6.8311 16.7615 7.0271C16.7999 7.10257 16.8348 7.21697 16.8544 7.45624C16.8745 7.7028 16.875 8.02298 16.875 8.50001V13.1667C16.875 13.6437 16.8745 13.9639 16.8544 14.2104C16.8348 14.4497 16.7999 14.5641 16.7615 14.6396C16.6616 14.8356 16.5022 14.9949 16.3062 15.0948C16.2308 15.1333 16.1164 15.1682 15.8771 15.1877C15.6305 15.2079 15.3104 15.2083 14.8333 15.2083H5.16667C4.68964 15.2083 4.36946 15.2079 4.1229 15.1877C3.88363 15.1682 3.76923 15.1333 3.69376 15.0948C3.49776 14.9949 3.3384 14.8356 3.23854 14.6396C3.20008 14.5641 3.16518 14.4497 3.14563 14.2104C3.12549 13.9639 3.125 13.6437 3.125 13.1667ZM9.20211 7.49996C9.01802 7.49996 8.86878 7.6492 8.86878 7.83329V10.867H7.47722C7.17943 10.867 7.03108 11.2277 7.24271 11.4372L10 14.1666L12.7573 11.4372C12.9689 11.2277 12.8206 10.867 12.5228 10.867H11.1312V7.83329C11.1312 7.6492 10.982 7.49996 10.7979 7.49996H9.20211Z"
fill="#62CD80"
/>
</svg>
);
};
export const PublishedIcon = () => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 18.125C14.4873 18.125 18.125 14.4873 18.125 10C18.125 5.51269 14.4873 1.875 10 1.875C5.51269 1.875 1.875 5.51269 1.875 10C1.875 14.4873 5.51269 18.125 10 18.125ZM9.99992 16.523C13.6024 16.523 16.5228 13.6026 16.5228 10.0001C16.5228 6.39761 13.6024 3.47722 9.99992 3.47722C6.39742 3.47722 3.47703 6.39761 3.47703 10.0001C3.47703 13.6026 6.39742 16.523 9.99992 16.523Z"
fill="#8699FF"
/>
<path
d="M7.13957 8.05468C8.34659 8.05468 9.33537 7.12035 9.42212 5.93548C9.57023 5.97414 9.72565 5.99472 9.88588 5.99472H10.8014C11.8126 5.99472 12.6324 5.17496 12.6324 4.16374C12.6324 3.15251 11.8126 2.33275 10.8014 2.33275H9.88588C9.03665 2.33275 8.32245 2.91091 8.11542 3.69509C7.81941 3.55535 7.48862 3.47722 7.13957 3.47722C5.8756 3.47722 4.85094 4.50182 4.85084 5.76577H3.70524V8.39782L7.59609 12.2887V12.9753C7.59609 13.8601 8.31338 14.5774 9.1982 14.5774V16.4084L10.457 17.4383L14.6912 15.264C14.6912 14.1264 13.7689 13.2042 12.6313 13.2042H12.288C12.288 11.3713 10.8022 9.88549 8.96932 9.88549H6.79503V8.02892C6.90741 8.04589 7.02246 8.05468 7.13957 8.05468Z"
fill="#8699FF"
/>
</svg>
);
};
export const CloudIcon = () => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 11.3744C2.5 13.837 4.51472 15.8333 7 15.8333L13.75 15.8333C15.8211 15.8333 17.5 14.1532 17.5 12.0807C17.5 10.5419 16.5744 9.12069 15.25 8.54163C15.1098 6.10205 13.07 4.16663 10.5744 4.16663C8.62616 4.16663 6.95578 5.40527 6.25 7.08329C4 7.44788 2.5 9.33336 2.5 11.3744Z"
stroke="#60A5FA"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.54757 7.5L7.5 13.3333H8.6993L10.0017 9.29884L11.3048 13.3333H12.5L10.4521 7.5H9.54757Z"
fill="#60A5FA"
/>
</svg>
);
};
export const LineIcon = () => {
return (
<svg
width="2"
height="20"
viewBox="0 0 2 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M1 0V20" stroke="black" strokeOpacity="0.15" />
</svg>
);
};

View File

@@ -0,0 +1,153 @@
import { Modal, ModalWrapper, ModalCloseButton } from '@affine/component';
import { FlexWrapper } from '@affine/component';
import { useState } from 'react';
import { CreateWorkspaceModal } from '../create-workspace';
import { Tooltip } from '@affine/component';
import { AddIcon, HelpCenterIcon } from '@blocksuite/icons';
import { useAppState } from '@/providers/app-state-provider';
import { useRouter } from 'next/router';
import { useTranslation } from '@affine/i18n';
import { LanguageMenu } from './SelectLanguageMenu';
import { LoginModal } from '../login-modal';
import { LogoutModal } from '../logout-modal';
import {
StyledCard,
StyledSplitLine,
StyleWorkspaceInfo,
StyleWorkspaceTitle,
StyledModalHeaderLeft,
StyledModalTitle,
StyledHelperContainer,
StyledModalContent,
StyledOperationWrapper,
StyleWorkspaceAdd,
StyledModalHeader,
} from './styles';
import { WorkspaceCard } from './WorkspaceCard';
import { Footer } from './Footer';
interface WorkspaceModalProps {
open: boolean;
onClose: () => void;
}
export const WorkspaceModal = ({ open, onClose }: WorkspaceModalProps) => {
const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false);
const { logout, dataCenter } = useAppState();
const router = useRouter();
const { t } = useTranslation();
const [loginOpen, setLoginOpen] = useState(false);
const [logoutOpen, setLogoutOpen] = useState(false);
return (
<>
<Modal open={open} onClose={onClose}>
<ModalWrapper
width={720}
height={690}
style={{
display: 'flex',
flexDirection: 'column',
}}
>
<StyledModalHeader>
<StyledModalHeaderLeft>
<StyledModalTitle>{t('My Workspaces')}</StyledModalTitle>
<Tooltip
content={t('Workspace description')}
placement="top-start"
disablePortal={true}
>
<StyledHelperContainer>
<HelpCenterIcon />
</StyledHelperContainer>
</Tooltip>
</StyledModalHeaderLeft>
<StyledOperationWrapper>
<LanguageMenu />
<StyledSplitLine />
<ModalCloseButton
data-testid="close-workspace-modal"
onClick={() => {
onClose();
}}
absolute={false}
/>
</StyledOperationWrapper>
</StyledModalHeader>
<StyledModalContent>
{dataCenter.workspaces.map((item, index) => {
return (
<WorkspaceCard
workspaceData={item}
onClick={workspaceData => {
router.replace(`/workspace/${workspaceData.id}`);
onClose();
}}
key={index}
></WorkspaceCard>
);
})}
<StyledCard
onClick={() => {
setCreateWorkspaceOpen(true);
}}
>
<FlexWrapper>
<StyleWorkspaceAdd className="add-icon">
<AddIcon fontSize={18} />
</StyleWorkspaceAdd>
</FlexWrapper>
<StyleWorkspaceInfo>
<StyleWorkspaceTitle>{t('New Workspace')}</StyleWorkspaceTitle>
<p>{t('Create Or Import')}</p>
</StyleWorkspaceInfo>
</StyledCard>
</StyledModalContent>
<Footer
onLogin={() => {
setLoginOpen(true);
}}
onLogout={() => {
setLogoutOpen(true);
}}
/>
</ModalWrapper>
</Modal>
<LoginModal
open={loginOpen}
onClose={() => {
setLoginOpen(false);
}}
/>
<LogoutModal
open={logoutOpen}
onClose={async wait => {
if (!wait) {
await logout();
if (dataCenter.workspaces.length === 0) {
router.push(`/workspace`);
} else {
router.push(`/workspace/${dataCenter.workspaces[0].id}`);
}
}
setLogoutOpen(false);
}}
/>
<CreateWorkspaceModal
open={createWorkspaceOpen}
onClose={() => {
setCreateWorkspaceOpen(false);
}}
/>
</>
);
};

View File

@@ -0,0 +1,164 @@
import {
displayFlex,
displayInlineFlex,
styled,
textEllipsis,
} from '@affine/component';
import { Button } from '@affine/component';
export const StyledSplitLine = styled.div(({ theme }) => {
return {
width: '1px',
height: '20px',
background: theme.colors.iconColor,
marginRight: '24px',
};
});
export const StyleWorkspaceInfo = styled.div(({ theme }) => {
return {
marginLeft: '15px',
p: {
height: '20px',
fontSize: theme.font.xs,
...displayFlex('flex-start', 'center'),
},
svg: {
marginRight: '10px',
},
};
});
export const StyleWorkspaceTitle = styled.div(({ theme }) => {
return {
fontSize: theme.font.base,
fontWeight: 600,
lineHeight: '24px',
marginBottom: '10px',
maxWidth: '200px',
...textEllipsis(1),
};
});
export const StyledCard = styled.div<{
active?: boolean;
}>(({ theme, active }) => {
const borderColor = active ? theme.colors.primaryColor : 'transparent';
return {
width: '310px',
height: '124px',
cursor: 'pointer',
padding: '16px',
boxShadow: '0px 0px 8px rgba(0, 0, 0, 0.1)',
borderRadius: '12px',
border: `1px solid ${borderColor}`,
...displayFlex('flex-start', 'flex-start'),
marginBottom: '24px',
':hover': {
background: theme.colors.hoverBackground,
'.add-icon': {
border: `1.5px dashed ${theme.colors.primaryColor}`,
svg: {
fill: theme.colors.primaryColor,
},
},
},
};
});
export const StyledFooter = styled('div')({
height: '84px',
padding: '0 40px',
...displayFlex('space-between', 'center'),
});
export const StyleUserInfo = styled.div(({ theme }) => {
return {
textAlign: 'left',
marginLeft: '16px',
flex: 1,
p: {
lineHeight: '24px',
color: theme.colors.iconColor,
},
'p:nth-child(1)': {
color: theme.colors.textColor,
fontWeight: 600,
},
};
});
export const StyledModalHeaderLeft = styled.div(() => {
return { ...displayFlex('flex-start', 'center') };
});
export const StyledModalTitle = styled.div(({ theme }) => {
return {
fontWeight: 600,
fontSize: theme.font.h6,
};
});
export const StyledHelperContainer = styled.div(({ theme }) => {
return {
color: theme.colors.iconColor,
marginLeft: '15px',
fontWeight: 400,
...displayFlex('center', 'center'),
};
});
export const StyledModalContent = styled('div')({
height: '534px',
padding: '8px 40px',
marginTop: '72px',
overflow: 'auto',
...displayFlex('space-between', 'flex-start', 'flex-start'),
flexWrap: 'wrap',
});
export const StyledOperationWrapper = styled.div(() => {
return {
...displayFlex('flex-end', 'center'),
};
});
export const StyleWorkspaceAdd = styled.div(() => {
return {
width: '58px',
height: '58px',
borderRadius: '100%',
textAlign: 'center',
background: '#f4f5fa',
border: '1.5px dashed #f4f5fa',
lineHeight: '58px',
marginTop: '2px',
};
});
export const StyledModalHeader = styled('div')(({ theme }) => {
return {
width: '100%',
height: '72px',
position: 'absolute',
left: 0,
top: 0,
background: theme.colors.pageBackground,
borderRadius: '24px 24px 0 0',
padding: '0 40px',
...displayFlex('space-between', 'center'),
};
});
export const StyledSignInButton = styled(Button)(({ theme }) => {
return {
fontWeight: 700,
paddingLeft: 0,
'.circle': {
width: '40px',
height: '40px',
borderRadius: '20px',
backgroundColor: theme.colors.innerHoverBackground,
flexShrink: 0,
marginRight: '16px',
...displayInlineFlex('center', 'center'),
},
};
});

View File

@@ -0,0 +1,16 @@
import { WorkspaceUnit } from '@affine/datacenter';
import { useTranslation } from '@affine/i18n';
import { Wrapper } from '@affine/component';
import { Button } from '@affine/component';
export const ExportPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const { t } = useTranslation();
console.log(workspace);
return (
<>
<Wrapper marginBottom="32px"> {t('Export Description')}</Wrapper>
<Button type="light" shape="circle" disabled>
{t('Export AFFINE backup file')}
</Button>
</>
);
};

View File

@@ -0,0 +1,89 @@
import { useState } from 'react';
import { Button } from '@affine/component';
import { Input } from '@affine/component';
import { toast } from '@affine/component';
import { WorkspaceUnit } from '@affine/datacenter';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { useTranslation } from '@affine/i18n';
import { EnableWorkspaceButton } from '../enable-workspace';
import { Wrapper, Content, FlexWrapper } from '@affine/component';
export const PublishPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const shareUrl = window.location.host + '/public-workspace/' + workspace.id;
const { publishWorkspace } = useWorkspaceHelper();
const { t } = useTranslation();
const [loaded, setLoaded] = useState(false);
const togglePublic = async (flag: boolean) => {
try {
await publishWorkspace(workspace.id.toString(), flag);
setLoaded(false);
} catch (e) {
toast(t('Failed to publish workspace'));
}
};
const copyUrl = () => {
navigator.clipboard.writeText(shareUrl);
toast(t('Copied link to clipboard'));
};
if (workspace.provider === 'affine') {
if (workspace.published) {
return (
<>
<Wrapper marginBottom="32px">{t('Published Description')}</Wrapper>
<Wrapper marginBottom="12px">
<Content weight="500">{t('Share with link')}</Content>
</Wrapper>
<FlexWrapper>
<Input width={582} value={shareUrl} disabled={true}></Input>
<Button
onClick={copyUrl}
type="light"
shape="circle"
style={{ marginLeft: '24px' }}
>
{t('Copy Link')}
</Button>
</FlexWrapper>
<Button
onClick={async () => {
setLoaded(true);
await togglePublic(false);
}}
loading={false}
type="danger"
shape="circle"
style={{ marginTop: '38px' }}
>
{t('Stop publishing')}
</Button>
</>
);
}
return (
<>
<Wrapper marginBottom="32px">{t('Publishing Description')}</Wrapper>
<Button
onClick={async () => {
setLoaded(true);
await togglePublic(true);
}}
loading={loaded}
type="light"
shape="circle"
>
{t('Publish to web')}
</Button>
</>
);
}
return (
<>
<Wrapper marginBottom="32px">{t('Publishing')}</Wrapper>
<EnableWorkspaceButton />
</>
);
};

View File

@@ -0,0 +1,106 @@
import {
StyledWorkspaceName,
StyledEmail,
// StyledDownloadCard,
// StyledDownloadCardDes,
} from './style';
import { WorkspaceUnit } from '@affine/datacenter';
import { useTranslation, Trans } from '@affine/i18n';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { EnableWorkspaceButton } from '../enable-workspace';
import { useAppState } from '@/providers/app-state-provider';
import { FlexWrapper, Content, Wrapper } from '@affine/component';
// // FIXME: Temporary solution, since the @blocksuite/icons is broken
// const ActiveIcon = () => {
// return (
// <svg
// width="24"
// height="24"
// viewBox="0 0 24 24"
// fill="none"
// xmlns="http://www.w3.org/2000/svg"
// >
// <path
// fillRule="evenodd"
// clipRule="evenodd"
// d="M2.25 12C2.25 6.61522 6.61522 2.25 12 2.25C17.3848 2.25 21.75 6.61522 21.75 12C21.75 17.3848 17.3848 21.75 12 21.75C6.61522 21.75 2.25 17.3848 2.25 12Z"
// fill="#6880FF"
// />
// <path
// fillRule="evenodd"
// clipRule="evenodd"
// d="M16.5068 8.44714C16.8121 8.72703 16.8328 9.20146 16.5529 9.5068L11.0529 15.5068C10.9146 15.6576 10.7208 15.7454 10.5163 15.7498C10.3118 15.7543 10.1143 15.675 9.96967 15.5303L7.46967 13.0303C7.17678 12.7374 7.17678 12.2626 7.46967 11.9697C7.76256 11.6768 8.23744 11.6768 8.53033 11.9697L10.4764 13.9158L15.4471 8.49321C15.727 8.18787 16.2015 8.16724 16.5068 8.44714Z"
// fill="white"
// />
// </svg>
// );
// };
export const SyncPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const { t } = useTranslation();
const { user } = useAppState();
if (workspace.provider === 'local') {
return (
<>
<FlexWrapper alignItems="center" style={{ marginBottom: '12px' }}>
<WorkspaceUnitAvatar
size={32}
name={workspace.name}
workspaceUnit={workspace}
style={{ marginRight: '12px' }}
/>
<StyledWorkspaceName>{workspace.name}&nbsp;</StyledWorkspaceName>
<Content weight={500}>{t('is a Local Workspace')}</Content>
</FlexWrapper>
<p>{t('Local Workspace Description')}</p>
<Wrapper marginTop="32px">
<EnableWorkspaceButton />
</Wrapper>
</>
);
}
return (
<>
<FlexWrapper alignItems="center" style={{ marginBottom: '12px' }}>
<WorkspaceUnitAvatar
size={32}
name={workspace.name}
workspaceUnit={workspace}
style={{ marginRight: '12px' }}
/>
<StyledWorkspaceName>{workspace.name}&nbsp;</StyledWorkspaceName>
<Content weight={500}>{t('is a Cloud Workspace')}</Content>
</FlexWrapper>
<Trans i18nKey="Cloud Workspace Description">
All data will be synchronised and saved to the AFFiNE account
<StyledEmail>
{{
email: '{' + user?.email + '}.',
}}
</StyledEmail>
</Trans>
{/*<Wrapper marginBottom="12px" marginTop="32px">*/}
{/* <Content weight="500">{t('Data sync mode')}</Content>*/}
{/*</Wrapper>*/}
{/*<FlexWrapper>*/}
{/* <StyledDownloadCard>*/}
{/* <ActiveIcon />*/}
{/* <Wrapper>{t('Download all data')}</Wrapper>*/}
{/* <StyledDownloadCardDes>*/}
{/* {t('It takes up more space on your device.')}*/}
{/* </StyledDownloadCardDes>*/}
{/* </StyledDownloadCard>*/}
{/* <StyledDownloadCard active>*/}
{/* <ActiveIcon />*/}
{/* <Wrapper>{t('Download core data')}</Wrapper>*/}
{/* <StyledDownloadCardDes>*/}
{/* {t('It takes up little space on your device.')}*/}
{/* </StyledDownloadCardDes>*/}
{/* </StyledDownloadCard>*/}
{/*</FlexWrapper>*/}
</>
);
};

View File

@@ -0,0 +1,185 @@
import { StyledInput, StyledProviderInfo, StyledAvatar } from './style';
import { StyledSettingKey, StyledRow } from '../style';
import { FlexWrapper, Content } from '@affine/component';
import { useState } from 'react';
import { Button } from '@affine/component';
import { useAppState } from '@/providers/app-state-provider';
import { WorkspaceDelete } from './delete';
import { WorkspaceLeave } from './leave';
import { UsersIcon } from '@blocksuite/icons';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { WorkspaceUnit } from '@affine/datacenter';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { useTranslation } from '@affine/i18n';
import { CloudIcon, LocalIcon } from '@/components/workspace-modal/icons';
import { CameraIcon } from './icons';
import { Upload } from '@/components/file-upload';
export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const [showDelete, setShowDelete] = useState<boolean>(false);
const [showLeave, setShowLeave] = useState<boolean>(false);
const [workspaceName, setWorkspaceName] = useState<string>(workspace?.name);
const { currentWorkspace, isOwner } = useAppState();
const { updateWorkspace } = useWorkspaceHelper();
const { t } = useTranslation();
const handleUpdateWorkspaceName = () => {
currentWorkspace &&
updateWorkspace({ name: workspaceName }, currentWorkspace);
};
const fileChange = async (file: File) => {
const blob = new Blob([file], { type: file.type });
currentWorkspace &&
(await updateWorkspace({ avatarBlob: blob }, currentWorkspace));
};
if (!workspace) {
return null;
}
return (
<>
<StyledRow>
<StyledSettingKey>{t('Workspace Avatar')}</StyledSettingKey>
<StyledAvatar disabled={!isOwner}>
{isOwner ? (
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={fileChange}
>
<>
<div className="camera-icon">
<CameraIcon></CameraIcon>
</div>
<WorkspaceUnitAvatar
size={72}
name={workspace.name}
workspaceUnit={workspace}
/>
</>
</Upload>
) : (
<WorkspaceUnitAvatar
size={72}
name={workspace.name}
workspaceUnit={workspace}
/>
)}
</StyledAvatar>
</StyledRow>
<StyledRow>
<StyledSettingKey>{t('Workspace Name')}</StyledSettingKey>
<FlexWrapper>
<StyledInput
width={284}
height={38}
value={workspaceName}
placeholder={t('Workspace Name')}
maxLength={15}
minLength={0}
disabled={!isOwner}
onChange={newName => {
setWorkspaceName(newName);
}}
></StyledInput>
{isOwner && (
<>
<Button
type="default"
shape="circle"
style={{ marginLeft: '24px' }}
onClick={() => {
setWorkspaceName(workspace.name);
}}
>
{t('Cancel')}
</Button>
<Button
type="light"
shape="circle"
style={{ marginLeft: '24px' }}
onClick={() => {
handleUpdateWorkspaceName();
}}
>
{t('Confirm')}
</Button>
</>
)}
</FlexWrapper>
</StyledRow>
<StyledRow>
<StyledSettingKey>{t('Workspace Type')}</StyledSettingKey>
<FlexWrapper>
{isOwner ? (
currentWorkspace?.provider === 'local' ? (
<FlexWrapper alignItems="center">
<LocalIcon />
<Content style={{ marginLeft: '15px' }}>
{t('Local Workspace')}
</Content>
</FlexWrapper>
) : (
<FlexWrapper alignItems="center">
<CloudIcon />
<Content style={{ marginLeft: '15px' }}>
{t('Available Offline')}
</Content>
</FlexWrapper>
)
) : (
<StyledProviderInfo>
<UsersIcon fontSize={20} color={'#FF646B'} />
{t('Joined Workspace')}
</StyledProviderInfo>
)}
</FlexWrapper>
</StyledRow>
<StyledRow>
<StyledSettingKey> {t('Delete Workspace')}</StyledSettingKey>
{isOwner ? (
<>
<Button
type="danger"
shape="circle"
style={{ borderRadius: '40px' }}
onClick={() => {
setShowDelete(true);
}}
>
{t('Delete Workspace')}
</Button>
<WorkspaceDelete
open={showDelete}
onClose={() => {
setShowDelete(false);
}}
workspace={workspace}
/>
</>
) : (
<>
<Button
type="danger"
shape="circle"
onClick={() => {
setShowLeave(true);
}}
>
{t('Leave Workspace')}
</Button>
<WorkspaceLeave
open={showLeave}
onClose={() => {
setShowLeave(false);
}}
/>
</>
)}
</StyledRow>
</>
);
};

View File

@@ -0,0 +1,101 @@
import { useRouter } from 'next/router';
import { Modal } from '@affine/component';
import { Input } from '@affine/component';
import {
StyledModalHeader,
StyledTextContent,
StyledModalWrapper,
StyledInputContent,
StyledButtonContent,
StyledWorkspaceName,
} from './style';
import { useState } from 'react';
import { ModalCloseButton } from '@affine/component';
import { Button } from '@affine/component';
import { WorkspaceUnit } from '@affine/datacenter';
import { Trans, useTranslation } from '@affine/i18n';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
interface WorkspaceDeleteProps {
open: boolean;
onClose: () => void;
workspace: WorkspaceUnit;
}
export const WorkspaceDelete = ({
open,
onClose,
workspace,
}: WorkspaceDeleteProps) => {
const [deleteStr, setDeleteStr] = useState<string>('');
const { t } = useTranslation();
const router = useRouter();
const { deleteWorkSpace } = useWorkspaceHelper();
const handlerInputChange = (workspaceName: string) => {
setDeleteStr(workspaceName);
};
const handleDelete = async () => {
await deleteWorkSpace();
onClose();
router.push(`/workspace`);
};
return (
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>
<ModalCloseButton onClick={onClose} />
<StyledModalHeader>{t('Delete Workspace')}?</StyledModalHeader>
{workspace.provider === 'local' ? (
<StyledTextContent>
<Trans i18nKey="Delete Workspace Description">
Deleting (
<StyledWorkspaceName>
{{ workspace: workspace.name }}
</StyledWorkspaceName>
) cannot be undone, please proceed with caution. along with all
its content.
</Trans>
</StyledTextContent>
) : (
<StyledTextContent>
<Trans i18nKey="Delete Workspace Description2">
Deleting (
<StyledWorkspaceName>
{{ workspace: workspace.name }}
</StyledWorkspaceName>
) will delete both local and cloud data, this operation cannot be
undone, please proceed with caution.
</Trans>
</StyledTextContent>
)}
<StyledInputContent>
<Input
onChange={handlerInputChange}
placeholder={t('Delete Workspace placeholder')}
value={deleteStr}
width={284}
height={42}
></Input>
</StyledInputContent>
<StyledButtonContent>
<Button shape="circle" onClick={onClose}>
{t('Cancel')}
</Button>
<Button
disabled={deleteStr.toLowerCase() !== 'delete'}
onClick={handleDelete}
type="danger"
shape="circle"
style={{ marginLeft: '24px' }}
>
{t('Delete')}
</Button>
</StyledButtonContent>
</StyledModalWrapper>
</Modal>
);
};
export default WorkspaceDelete;

View File

@@ -0,0 +1 @@
export { WorkspaceDelete } from './Delete';

View File

@@ -0,0 +1,80 @@
import { styled } from '@affine/component';
export const StyledModalWrapper = styled('div')(({ theme }) => {
return {
position: 'relative',
padding: '0px',
width: '560px',
background: theme.colors.popoverBackground,
borderRadius: '12px',
height: '312px',
};
});
export const StyledModalHeader = styled('div')(({ theme }) => {
return {
margin: '44px 0px 12px 0px',
width: '560px',
fontWeight: '600',
fontSize: '20px;',
textAlign: 'center',
color: theme.colors.popoverColor,
};
});
// export const StyledModalContent = styled('div')(({ theme }) => {});
export const StyledTextContent = styled('div')(() => {
return {
margin: 'auto',
width: '425px',
fontFamily: 'Avenir Next',
fontStyle: 'normal',
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
textAlign: 'left',
};
});
export const StyledInputContent = styled('div')(({ theme }) => {
return {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '32px 0',
fontSize: theme.font.base,
};
});
export const StyledButtonContent = styled('div')(() => {
return {
position: 'absolute',
left: '50%',
transform: 'translateX(-50%)',
bottom: '32px',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
};
});
export const StyledWorkspaceName = styled('span')(() => {
return {
color: '#E8178A',
};
});
// export const StyledCancelButton = styled(Button)(({ theme }) => {
// return {
// width: '100px',
// justifyContent: 'center',
// };
// });
// export const StyledDeleteButton = styled(Button)(({ theme }) => {
// return {
// width: '100px',
// justifyContent: 'center',
// };
// });

View File

@@ -0,0 +1,20 @@
export const CameraIcon = () => {
return (
<span>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.6236 4.25001C10.635 4.25001 10.6467 4.25002 10.6584 4.25002H13.3416C13.3533 4.25002 13.365 4.25001 13.3764 4.25001C13.5609 4.24995 13.7105 4.2499 13.8543 4.26611C14.5981 4.34997 15.2693 4.75627 15.6826 5.38026C15.7624 5.50084 15.83 5.63398 15.9121 5.79586C15.9173 5.80613 15.9226 5.81652 15.9279 5.82703C15.9538 5.87792 15.9679 5.90562 15.9789 5.9261C15.9832 5.9341 15.9857 5.93861 15.9869 5.94065C16.0076 5.97069 16.0435 5.99406 16.0878 5.99905L16.0849 5.99877C16.0849 5.99877 16.0907 5.99918 16.1047 5.99947C16.1286 5.99998 16.1604 6.00002 16.2181 6.00002L17.185 6.00001C17.6577 6 18.0566 5.99999 18.3833 6.02627C18.7252 6.05377 19.0531 6.11364 19.3656 6.27035C19.8402 6.50842 20.2283 6.88944 20.4723 7.36077C20.6336 7.67233 20.6951 7.99944 20.7232 8.33858C20.75 8.66166 20.75 9.05554 20.75 9.51992V16.2301C20.75 16.6945 20.75 17.0884 20.7232 17.4114C20.6951 17.7506 20.6336 18.0777 20.4723 18.3893C20.2283 18.8606 19.8402 19.2416 19.3656 19.4797C19.0531 19.6364 18.7252 19.6963 18.3833 19.7238C18.0566 19.75 17.6578 19.75 17.185 19.75H6.81497C6.34225 19.75 5.9434 19.75 5.61668 19.7238C5.27477 19.6963 4.94688 19.6364 4.63444 19.4797C4.15978 19.2416 3.77167 18.8606 3.52771 18.3893C3.36644 18.0777 3.30494 17.7506 3.27679 17.4114C3.24998 17.0884 3.24999 16.6945 3.25 16.2302V9.51987C3.24999 9.05551 3.24998 8.66164 3.27679 8.33858C3.30494 7.99944 3.36644 7.67233 3.52771 7.36077C3.77167 6.88944 4.15978 6.50842 4.63444 6.27035C4.94688 6.11364 5.27477 6.05377 5.61668 6.02627C5.9434 5.99999 6.34225 6 6.81498 6.00001L7.78191 6.00002C7.83959 6.00002 7.87142 5.99998 7.8953 5.99947C7.90607 5.99924 7.91176 5.99897 7.91398 5.99884C7.95747 5.99343 7.99267 5.9703 8.01312 5.94066C8.01429 5.93863 8.01684 5.93412 8.02113 5.9261C8.0321 5.90561 8.04622 5.87791 8.07206 5.82703C8.07739 5.81653 8.08266 5.80615 8.08787 5.79588C8.17004 5.63397 8.23759 5.50086 8.31745 5.38026C8.73067 4.75627 9.40192 4.34997 10.1457 4.26611C10.2895 4.2499 10.4391 4.24995 10.6236 4.25001ZM10.6584 5.75002C10.422 5.75002 10.3627 5.75114 10.3138 5.75666C10.0055 5.79142 9.73316 5.95919 9.56809 6.20845C9.54218 6.24758 9.51544 6.29761 9.40943 6.50633C9.40611 6.51287 9.40274 6.5195 9.39934 6.52622C9.36115 6.60161 9.31758 6.68761 9.26505 6.76694C8.9964 7.17261 8.56105 7.4354 8.08026 7.48961C7.98625 7.50021 7.89021 7.50011 7.80434 7.50003C7.79678 7.50002 7.7893 7.50002 7.78191 7.50002H6.84445C6.33444 7.50002 5.99634 7.50058 5.73693 7.52144C5.48594 7.54163 5.37478 7.57713 5.30693 7.61115C5.11257 7.70864 4.95675 7.86306 4.85983 8.05029C4.82733 8.11308 4.79194 8.21816 4.77165 8.46266C4.7506 8.71626 4.75 9.0474 4.75 9.55001V16.2C4.75 16.7026 4.7506 17.0338 4.77165 17.2874C4.79194 17.5319 4.82733 17.6369 4.85983 17.6997C4.95675 17.887 5.11257 18.0414 5.30693 18.1389C5.37478 18.1729 5.48594 18.2084 5.73693 18.2286C5.99634 18.2494 6.33444 18.25 6.84445 18.25H17.1556C17.6656 18.25 18.0037 18.2494 18.2631 18.2286C18.5141 18.2084 18.6252 18.1729 18.6931 18.1389C18.8874 18.0414 19.0433 17.887 19.1402 17.6997C19.1727 17.6369 19.2081 17.5319 19.2283 17.2874C19.2494 17.0338 19.25 16.7026 19.25 16.2V9.55001C19.25 9.0474 19.2494 8.71626 19.2283 8.46266C19.2081 8.21816 19.1727 8.11308 19.1402 8.05029C19.0433 7.86306 18.8874 7.70864 18.6931 7.61115C18.6252 7.57713 18.5141 7.54163 18.2631 7.52144C18.0037 7.50058 17.6656 7.50002 17.1556 7.50002H16.2181C16.2107 7.50002 16.2032 7.50002 16.1957 7.50003C16.1098 7.50011 16.0138 7.50021 15.9197 7.48961C15.4389 7.4354 15.0036 7.17261 14.735 6.76694C14.6824 6.68761 14.6389 6.60163 14.6007 6.52622C14.5973 6.5195 14.5939 6.51287 14.5906 6.50633C14.4846 6.29763 14.4578 6.24758 14.4319 6.20846C14.2668 5.95919 13.9945 5.79142 13.6862 5.75666C13.6373 5.75114 13.578 5.75002 13.3416 5.75002H10.6584ZM12 11C10.9303 11 10.0833 11.8506 10.0833 12.875C10.0833 13.8995 10.9303 14.75 12 14.75C13.0697 14.75 13.9167 13.8995 13.9167 12.875C13.9167 11.8506 13.0697 11 12 11ZM8.58333 12.875C8.58333 11 10.1242 9.50002 12 9.50002C13.8758 9.50002 15.4167 11 15.4167 12.875C15.4167 14.7501 13.8758 16.25 12 16.25C10.1242 16.25 8.58333 14.7501 8.58333 12.875Z"
fill="white"
/>
</svg>
</span>
);
};

View File

@@ -0,0 +1 @@
export { GeneralPage } from './General';

View File

@@ -0,0 +1,54 @@
import { Modal } from '@affine/component';
import {
StyledModalHeader,
StyledTextContent,
StyledModalWrapper,
StyledButtonContent,
} from './style';
import { ModalCloseButton } from '@affine/component';
import { Button } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
// import { getDataCenter } from '@affine/datacenter';
// import { useAppState } from '@/providers/app-state-provider';
interface WorkspaceDeleteProps {
open: boolean;
onClose: () => void;
}
export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => {
const { leaveWorkSpace } = useWorkspaceHelper();
const { t } = useTranslation();
const handleLeave = async () => {
await leaveWorkSpace();
onClose();
};
return (
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>
<ModalCloseButton onClick={onClose} />
<StyledModalHeader>{t('Leave Workspace')}</StyledModalHeader>
<StyledTextContent>
{t('Leave Workspace Description')}
</StyledTextContent>
<StyledButtonContent>
<Button shape="circle" onClick={onClose}>
{t('Cancel')}
</Button>
<Button
onClick={handleLeave}
type="danger"
shape="circle"
style={{ marginLeft: '24px' }}
>
{t('Leave')}
</Button>
</StyledButtonContent>
</StyledModalWrapper>
</Modal>
);
};
export default WorkspaceLeave;

View File

@@ -0,0 +1 @@
export { WorkspaceLeave } from './Leave';

View File

@@ -0,0 +1,46 @@
import { styled } from '@affine/component';
export const StyledModalWrapper = styled('div')(({ theme }) => {
return {
position: 'relative',
padding: '0px',
width: '460px',
background: theme.colors.popoverBackground,
borderRadius: '12px',
};
});
export const StyledModalHeader = styled('div')(({ theme }) => {
return {
margin: '44px 0px 12px 0px',
width: '460px',
fontWeight: '600',
fontSize: '20px;',
textAlign: 'center',
color: theme.colors.popoverColor,
};
});
// export const StyledModalContent = styled('div')(({ theme }) => {});
export const StyledTextContent = styled('div')(() => {
return {
margin: 'auto',
width: '425px',
fontFamily: 'Avenir Next',
fontStyle: 'normal',
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
textAlign: 'center',
};
});
export const StyledButtonContent = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '0px 0 32px 0',
};
});

View File

@@ -0,0 +1,47 @@
import { styled } from '@affine/component';
import { Input } from '@affine/component';
export const StyledInput = styled(Input)(({ theme }) => {
return {
border: `1px solid ${theme.colors.borderColor}`,
borderRadius: '10px',
fontSize: theme.font.sm,
};
});
export const StyledProviderInfo = styled('p')(({ theme }) => {
return {
color: theme.colors.iconColor,
svg: {
verticalAlign: 'sub',
marginRight: '10px',
},
};
});
export const StyledAvatar = styled('div')(
({ disabled }: { disabled: boolean }) => {
return {
position: 'relative',
marginRight: '20px',
cursor: disabled ? 'default' : 'pointer',
':hover': {
'.camera-icon': {
display: 'block',
},
},
'.camera-icon': {
position: 'absolute',
display: 'none',
width: '100%',
height: '100%',
borderRadius: '50%',
backgroundColor: 'rgba(60, 61, 63, 0.5)',
top: 0,
left: 0,
textAlign: 'center',
lineHeight: '72px',
},
};
}
);

View File

@@ -0,0 +1,5 @@
export * from './general';
export * from './ExportPage';
export * from './member';
export * from './SyncPage';
export * from './PublishPage';

View File

@@ -0,0 +1,235 @@
import { EmailIcon } from '@blocksuite/icons';
import { styled } from '@affine/component';
import { Modal, ModalWrapper, ModalCloseButton } from '@affine/component';
import { Button } from '@affine/component';
import { Input } from '@affine/component';
import { useState } from 'react';
import { MuiAvatar } from '@affine/component';
import useMembers from '@/hooks/use-members';
import { User } from '@affine/datacenter';
import { useTranslation } from '@affine/i18n';
interface LoginModalProps {
open: boolean;
onClose: () => void;
workspaceId: string;
onInviteSuccess: () => void;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const debounce = <T extends (...args: any) => any>(
fn: T,
time?: number,
immediate?: boolean
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): ((...args: any) => any) => {
let timeoutId: null | number;
let defaultImmediate = immediate || false;
const delay = time || 300;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (...args: any) => {
if (defaultImmediate) {
fn.apply(this, args);
defaultImmediate = false;
return;
}
if (timeoutId) {
clearTimeout(timeoutId);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
timeoutId = setTimeout(() => {
fn.apply(this, args);
timeoutId = null;
}, delay);
};
};
const gmailReg = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@gmail\.com$/;
export const InviteMemberModal = ({
open,
onClose,
onInviteSuccess,
}: LoginModalProps) => {
const [email, setEmail] = useState<string>('');
const [showMember, setShowMember] = useState<boolean>(false);
const [showTip, setShowTip] = useState<boolean>(false);
const [userData, setUserData] = useState<User | null>(null);
const { inviteMember, getUserByEmail } = useMembers();
const { t } = useTranslation();
const inputChange = (value: string) => {
setShowMember(true);
if (gmailReg.test(value)) {
setEmail(value);
setShowTip(false);
getUserByEmail(value).then(data => {
if (data?.name) {
setUserData(data);
setShowTip(false);
}
});
} else {
setShowTip(true);
}
};
return (
<div>
<Modal open={open} onClose={onClose}>
<ModalWrapper width={460} height={236}>
<Header>
<ModalCloseButton
onClick={() => {
onClose();
setEmail('');
}}
/>
</Header>
<Content>
<ContentTitle>{t('Invite Members')}</ContentTitle>
<InviteBox>
<Input
width={360}
value={email}
onChange={inputChange}
onBlur={() => {
setShowMember(false);
}}
placeholder={t('Invite placeholder')}
></Input>
{showMember ? (
<Members>
{showTip ? (
<NoFind>{t('Non-Gmail')}</NoFind>
) : (
<Member>
{userData?.avatar ? (
<MuiAvatar src={userData?.avatar}></MuiAvatar>
) : (
<MemberIcon>
<EmailIcon></EmailIcon>
</MemberIcon>
)}
<Email>{email}</Email>
{/* <div>invited</div> */}
</Member>
)}
</Members>
) : (
<></>
)}
</InviteBox>
</Content>
<Footer>
<Button
shape="circle"
type="primary"
style={{ width: '364px', height: '38px', borderRadius: '40px' }}
onClick={async () => {
await inviteMember(email);
setEmail('');
onInviteSuccess();
}}
>
{t('Invite')}
</Button>
</Footer>
</ModalWrapper>
</Modal>
</div>
);
};
const Header = styled('div')({
position: 'relative',
height: '44px',
});
const Content = styled('div')({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
});
const ContentTitle = styled('h1')({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'center',
paddingBottom: '16px',
});
const Footer = styled('div')({
height: '102px',
margin: '32px 0',
textAlign: 'center',
});
const InviteBox = styled('div')({
position: 'relative',
});
const Members = styled('div')(({ theme }) => {
return {
position: 'absolute',
width: '100%',
background: theme.colors.pageBackground,
textAlign: 'left',
zIndex: 1,
borderRadius: '0px 10px 10px 10px',
height: '56px',
padding: '8px 12px',
input: {
'&::placeholder': {
color: theme.colors.placeHolderColor,
},
},
};
});
const NoFind = styled('div')(({ theme }) => {
return {
color: theme.colors.iconColor,
fontSize: theme.font.sm,
lineHeight: '40px',
userSelect: 'none',
width: '100%',
};
});
const Member = styled('div')(({ theme }) => {
return {
color: theme.colors.iconColor,
fontSize: theme.font.sm,
lineHeight: '40px',
userSelect: 'none',
display: 'flex',
};
});
const MemberIcon = styled('div')(({ theme }) => {
return {
width: '40px',
height: '40px',
borderRadius: '50%',
color: theme.colors.primaryColor,
background: '#F5F5F5',
marginRight: '8px',
textAlign: 'center',
lineHeight: '45px',
// icon size
fontSize: '20px',
overflow: 'hidden',
img: {
width: '100%',
height: '100%',
},
};
});
const Email = styled('div')(({ theme }) => {
return {
flex: '1',
color: theme.colors.popoverColor,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});

View File

@@ -0,0 +1,178 @@
import {
StyledMemberAvatar,
StyledMemberButtonContainer,
StyledMemberEmail,
StyledMemberInfo,
StyledMemberListContainer,
StyledMemberListItem,
StyledMemberName,
StyledMemberNameContainer,
StyledMemberRoleContainer,
StyledMemberTitleContainer,
StyledMoreVerticalButton,
StyledMemberContainer,
} from './style';
import { Wrapper } from '@affine/component';
import { MoreVerticalIcon, EmailIcon, TrashIcon } from '@blocksuite/icons';
import { useState } from 'react';
import { Button, IconButton } from '@affine/component';
import { InviteMemberModal } from './InviteMemberModal';
import { Menu, MenuItem } from '@affine/component';
import { Empty } from '@affine/component';
import { WorkspaceUnit } from '@affine/datacenter';
import { useConfirm } from '@/providers/ConfirmProvider';
import { toast } from '@affine/component';
import useMembers from '@/hooks/use-members';
import Loading from '@/components/loading';
import { FlexWrapper } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { EnableWorkspaceButton } from '@/components/enable-workspace';
export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
const { members, removeMember, loaded } = useMembers();
const { t } = useTranslation();
const { confirm } = useConfirm();
if (workspace.provider === 'affine') {
return (
<StyledMemberContainer>
<StyledMemberListContainer>
{!loaded && (
<FlexWrapper justifyContent="center">
<Loading size={25} />
</FlexWrapper>
)}
{loaded && members.length === 0 && (
<Empty width={648} sx={{ marginTop: '60px' }} height={300} />
)}
{loaded && members.length > 0 && (
<>
<StyledMemberTitleContainer>
<StyledMemberNameContainer>
{t('Users')} ({members.length})
</StyledMemberNameContainer>
<StyledMemberRoleContainer>
{t('Access level')}
</StyledMemberRoleContainer>
<div style={{ width: '24px', paddingRight: '48px' }}></div>
</StyledMemberTitleContainer>
{members.map((member, index) => {
const user = Object.assign(
{
avatar_url: '',
email: '',
id: '',
name: '',
},
member.user
);
return (
<StyledMemberListItem key={index}>
<StyledMemberNameContainer>
<StyledMemberAvatar
alt="member avatar"
src={user.avatar_url}
>
<EmailIcon />
</StyledMemberAvatar>
<StyledMemberInfo>
<StyledMemberName>{user.name}</StyledMemberName>
<StyledMemberEmail>
{member.user.email}
</StyledMemberEmail>
</StyledMemberInfo>
</StyledMemberNameContainer>
<StyledMemberRoleContainer>
{member.accepted
? member.type !== 99
? t('Member')
: t('Owner')
: t('Pending')}
</StyledMemberRoleContainer>
<StyledMoreVerticalButton>
{member.type === 99 ? (
<></>
) : (
<Menu
content={
<>
<MenuItem
onClick={async () => {
const confirmRemove = await confirm({
title: t('Delete Member?'),
content: t('will delete member'),
confirmText: t('Delete'),
confirmType: 'danger',
});
if (!confirmRemove) {
return;
}
await removeMember(member.id);
toast(
t('Member has been removed', {
name: user.name,
})
);
}}
icon={<TrashIcon />}
>
{t('Delete')}
</MenuItem>
</>
}
placement="bottom-end"
disablePortal={true}
>
<IconButton>
<MoreVerticalIcon />
</IconButton>
</Menu>
)}
</StyledMoreVerticalButton>
</StyledMemberListItem>
);
})}
</>
)}
</StyledMemberListContainer>
<StyledMemberButtonContainer>
<Button
onClick={() => {
setIsInviteModalShow(true);
}}
type="primary"
shape="circle"
>
{t('Invite Members')}
</Button>
<InviteMemberModal
onClose={() => {
setIsInviteModalShow(false);
}}
onInviteSuccess={() => {
setIsInviteModalShow(false);
// refreshMembers();
}}
workspaceId={workspace.id}
open={isInviteModalShow}
></InviteMemberModal>
</StyledMemberButtonContainer>
</StyledMemberContainer>
);
}
return (
<Wrapper
style={{
fontWeight: '500',
fontSize: '18px',
}}
>
<Wrapper marginBottom="32px">{t('Collaboration Description')}</Wrapper>
<EnableWorkspaceButton />
</Wrapper>
);
};

View File

@@ -0,0 +1 @@
export * from './MembersPage';

View File

@@ -0,0 +1,99 @@
import { styled } from '@affine/component';
import { MuiAvatar } from '@affine/component';
export const StyledMemberTitleContainer = styled('li')(() => {
return {
display: 'flex',
fontWeight: '500',
marginBottom: '32px',
flex: 1,
};
});
export const StyledMemberContainer = styled('div')(() => {
return {
display: 'flex',
height: '100%',
flexDirection: 'column',
};
});
export const StyledMemberAvatar = styled(MuiAvatar)(() => {
return { height: '40px', width: '40px' };
});
export const StyledMemberNameContainer = styled('div')(() => {
return {
display: 'flex',
alignItems: 'center',
flex: '2 0 402px',
};
});
export const StyledMemberRoleContainer = styled('div')(() => {
return {
display: 'flex',
alignItems: 'center',
flex: '1 0 222px',
};
});
export const StyledMemberListContainer = styled('ul')(() => {
return {
overflowY: 'scroll',
width: '100%',
flex: 1,
};
});
export const StyledMemberListItem = styled('li')(() => {
return {
display: 'flex',
alignItems: 'center',
height: '72px',
width: '100%',
};
});
export const StyledMemberInfo = styled('div')(() => {
return {
paddingLeft: '12px',
};
});
export const StyledMemberName = styled('div')(({ theme }) => {
return {
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
color: theme.colors.textColor,
};
});
export const StyledMemberEmail = styled('div')(({ theme }) => {
return {
fontWeight: '400',
fontSize: '16px',
lineHeight: '22px',
color: theme.colors.iconColor,
};
});
export const StyledMemberButtonContainer = styled('div')(() => {
return {
position: 'absolute',
bottom: '0',
marginBottom: '20px',
};
});
export const StyledMoreVerticalButton = styled('button')(() => {
return {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '24px',
height: '24px',
cursor: 'pointer',
paddingRight: '48px',
};
});

View File

@@ -0,0 +1,115 @@
import { styled } from '@affine/component';
import { FlexWrapper } from '@affine/component';
export const StyledSettingContainer = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'column',
padding: '0 34px 20px 48px',
height: '100vh',
};
});
export const StyledSettingSidebar = styled('div')(() => {
{
return {
marginTop: '48px',
};
}
});
export const StyledSettingContent = styled('div')(() => {
return {
overflow: 'auto',
flex: 1,
paddingTop: '48px',
};
});
export const StyledSettingTabContainer = styled('ul')(() => {
{
return {
display: 'flex',
};
}
});
export const WorkspaceSettingTagItem = styled('li')<{ isActive?: boolean }>(
({ theme, isActive }) => {
{
return {
display: 'flex',
margin: '0 48px 0 0',
height: '34px',
color: isActive ? theme.colors.primaryColor : theme.colors.textColor,
fontWeight: '500',
fontSize: theme.font.h6,
lineHeight: theme.font.lineHeight,
cursor: 'pointer',
transition: 'all 0.15s ease',
borderBottom: `2px solid ${
isActive ? theme.colors.primaryColor : 'none'
}`,
':hover': { color: theme.colors.primaryColor },
};
}
}
);
export const StyledSettingKey = styled.div(({ theme }) => {
return {
width: '140px',
textAlign: 'right',
fontSize: theme.font.base,
fontWeight: 500,
marginRight: '56px',
flexShrink: 0,
};
});
export const StyledRow = styled(FlexWrapper)(() => {
return {
marginBottom: '32px',
};
});
export const StyledWorkspaceName = styled('span')(({ theme }) => {
return {
fontWeight: '400',
fontSize: theme.font.h6,
};
});
export const StyledEmail = styled('span')(() => {
return {
color: '#E8178A',
};
});
// export const StyledDownloadCard = styled.div<{ active?: boolean }>(
// ({ theme, active }) => {
// return {
// width: '240px',
// height: '86px',
// border: '1px solid',
// borderColor: active
// ? theme.colors.primaryColor
// : theme.colors.borderColor,
// borderRadius: '10px',
// padding: '8px 12px',
// position: 'relative',
// ':not(:last-of-type)': {
// marginRight: '24px',
// },
// svg: {
// display: active ? 'block' : 'none',
// ...positionAbsolute({ top: '-12px', right: '-12px' }),
// },
// };
// }
// );
// export const StyledDownloadCardDes = styled.div(({ theme }) => {
// return {
// fontSize: theme.font.sm,
// color: theme.colors.iconColor,
// };
// });

View File

@@ -0,0 +1,37 @@
import type { ReactNode } from 'react';
import { styled } from '@affine/component';
import { WorkspaceItemWrapper, WorkspaceItemContent } from './styles';
interface ListItemProps {
name: string;
icon: ReactNode;
onClick: () => void;
}
export const ListItem = ({ name, icon, onClick }: ListItemProps) => {
return (
<WorkspaceItemWrapper onClick={onClick}>
<StyledIconWrapper>{icon}</StyledIconWrapper>
<WorkspaceItemContent>
<Name title={name}>{name}</Name>
</WorkspaceItemContent>
</WorkspaceItemWrapper>
);
};
const Name = styled('div')(({ theme }) => {
return {
color: theme.colors.quoteColor,
fontSize: theme.font.sm,
fontWeight: 400,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});
const StyledIconWrapper = styled('div')({
width: '20px',
height: '20px',
fontSize: '20px',
});

Some files were not shown because too many files have changed in this diff Show More